#!/usr/bin/env python3
'''
Author: zhuanganmin@kylinos.cn
Date: 2025-08-26 11:04:11
Description: This script is designed for greater security on Kylinos systems
'''
import sys
import os
import re
import pwd
import spwd
import grp
import json
import glob
import shutil
import tempfile
import logging
import subprocess
import ctypes
import traceback
import string
import argparse
from pathlib import Path
from typing import Dict, Optional, Union,List, Tuple
from datetime import datetime, timedelta
from collections import OrderedDict
import hashlib
from threading import Thread

if os.environ.get('DISPLAY'):
    os.environ['NO_AT_BRIDGE']='1'
    import gi
    gi.require_version('Gtk', '3.0')
    from gi.repository import Gtk, GObject,Gdk, Pango, GLib


LOG_FILE= '/var/log/sectool.log'


def Singleton(cls):
    '''
    Singleton decorator
    '''
    _instance = {}

    def _singleton(*args, **kargs):
        if cls not in _instance:
            _instance[cls] = cls(*args, **kargs)
            _instance[cls].__name__ = cls.__name__
        return _instance[cls]
    
     

    return _singleton


class SingletonSerializable:
    _instance = None  # 单例实例

    def __new__(cls, *args, **kwargs):
        # 确保只创建一个实例
        if not cls._instance:
            cls._instance = super().__new__(cls)
        return cls._instance

    def dump(self):
        '''导出所有属性到字典（排除单例控制属性）'''
        return {k: v for k, v in self.__dict__.items() if k not in ('_initialized')}

    def load(self, data):
        '''根据字典更新实例属性'''
        self.__dict__.update(data)

    def __repr__(self):
        return "<{} at {}>".format(self.__class__.__name__,hex(id(self)))
    



class SecBase:
    
    _instance = None  # 单例实例
    backup_dir =  "/var/log/sectool/{}".format(datetime.now().strftime("%Y%m%d"))

    def __new__(cls, *args, **kwargs):
        # 确保只创建一个实例
        if not cls._instance:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        self.logger = logging.getLogger(__name__)
        self.grub_file = '/etc/default/grub'
        self.os_info = {}
        self.get_os_info()
        self.config = {
             "param" : { 'default': '', 
                         'range': [], 
                         'type': 'scale/combo/check/entry/spin/...', 
                         'msg' :'描述' },
        }
        
        
    def read_config(self) -> dict:
        ''' 子类实现 '''
        pass
    
    def write_config(self, value:dict = None) -> bool:
        ''' 子类实现 '''
        pass
    
    def update_config(self,updates:dict):
        for key,value in updates.items():
            self.config[key]['default'] =  value   
            
    def dump_config(self) -> dict:
        tmp_data = {}
        for key,value in self.config.items():
            tmp_data[key] = value['default']
        return tmp_data
    
    def get_config_item(self, param: str):
        return self.config.get(param).get('default')   
     
    def set_config_item(self, param: str, value: str) -> bool:
        self.config[param]['default'] = value
    
    
    
    def get_os_info(self) -> dict:
        '''
        获取操作系统信息
        :reutrn  dict: 系统信息
        '''
        uname = os.uname()
        self.os_info = {
            'arch': uname.machine,
            'kernel': uname.release,
            'hostname': uname.nodename,
            'os_version': '',
            'package_type':''
        }
        info_files = [ '/etc/.kyinfo' ,  '/etc/.productinfo']
        for file in info_files:
            if os.path.exists(file):
                with open(file,'r') as f:
                    for line in f:
                        if 'dist_id' in line:
                            version = line.split('=')[1]
                            self.os_info['os_version'] = version.split(maxsplit=1)[0]
                        elif 'release' in line:
                            version = line.split(maxsplit=1)[1]
                            self.os_info['os_version'] = version.split(maxsplit=1)[0]
        if shutil.which('apt'):
            self.os_info['package_type'] = 'apt'
        elif shutil.which('rpm'):
            self.os_info['package_type'] = 'rpm'
        return self.os_info

    def get_pam_modules(self) -> List:
        '''
        获取系统安装的所有PAM模块
        returns: List: 所有PAM模块
        '''
        pam_dirs = [
            '/lib/{0}-linux-gnu/security'.format(os.uname().machine),  # Ubuntu/Debian常见路径
            '/lib64/security',                 # CentOS/RHEL常见路径
            '/usr/lib/{0}-linux-gnu/security'.format(os.uname().machine),
            '/usr/lib64/security',
            '/usr/lib/security'
        ]
        self.logger.debug('pam_dirs = {}'.format(pam_dirs))
        modules = set()
        for pam_dir in pam_dirs:
            if os.path.exists(pam_dir):
                for entry in os.listdir(pam_dir):
                    if entry.endswith('.so'):
                        modules.add(entry[:-3])  # 去掉.so后缀
        modules = sorted(modules)
        self.logger.debug('modules = {}'.format(modules))
        return modules            
            
    def merge_dict(self, source: dict,override : dict):
        '''
        合并两个字典
        :param: source: 原始字典
        :param: override: 要覆盖的字典
        :returns: dict: 合并后的字典
        '''
        for k,v in override.items():
            if k in source:
                if isinstance(override[k], dict)  and isinstance(source[k], dict) :
                    self.merge_dict(source[k],override[k])
                    continue
            source[k] = override[k]
                
        return source
    
    def update_grub_file(self) -> bool :
        
        cmd = []
        if self.get_os_info()['package_type'] =='apt':
            cmd.append('update-grub')
        else:
            cmd.append('grub2-mkconfig')
            cmd.append('-o')
            if os.path.exists('/sys/firmware/efi'):
                cmd.append('/boot/efi/EFI/kylin/grub.cfg')
            else:
                cmd.append('/boot/grub2/grub.cfg')

        # 更新GRUB配置
        success, output = self.run_command(cmd)
        if not success:
            print("Failed to update GRUB: {}".format(output))
            return False
        return True
    
    def update_grub_cmdline(self, param: str, value: Optional[str] = None, remove: bool = False ,  grub_file:str =  None) -> bool:
        '''
        更新GRUB_CMDLINE_LINUX参数     
        :param: param: 要添加/修改的参数名
        :param: value: 参数值(可选)
        :param: remove: 是否删除参数
        :returns: bool: 是否更新成功
        '''
        if not grub_file:
            grub_file = self.grub_file
            
        if not os.path.exists(grub_file):
            self.logger.error("GRUB config file not found: {}".format(grub_file))
            return False
        
        grub_backup = self.backup_file(grub_file)
        
        try:
            with open(grub_file, 'r') as f:
                lines = f.readlines()
            
            new_lines = []
            param_str = "{}={}".format(param,value) if value else param
            param_pattern = re.compile(r'^{}(?:=[^\s"]*)?'.format(param))
            self.logger.debug(param_str)

            is_changed = False
            for line in lines:
                # 解析当前命令行参数
                match = re.match(r'^\s*(GRUB_CMDLINE_LINUX(?:_DEFAULT)?)\s*=\s*"([^"]*)"\s*$', line)
                if match and not is_changed:
                    key = match.group(1)
                    params = match.group(2).split()
                    self.logger.debug('{} = {}'.format(key, params))
                    
                    # 添加/删除参数
                    new_params = []
                    found = False
                    for p in params:
                        if param_pattern.match(p):
                            found = True
                            if not remove:
                                new_params.append(param_str)
                        else:
                            new_params.append(p)
                    
                    if not found and not remove:
                        new_params.append(param_str)
                    
                    new_line = '{}="{}"\n'.format(key ,' '.join(new_params))
                    new_lines.append(new_line)
                    is_changed = True
                else:
                    new_lines.append(line)
        
            if is_changed:
                with open(grub_file, 'w') as f:
                    f.writelines(new_lines)
                if self.update_grub_file():
                    return True 
                else: 
                    # 恢复备份
                    shutil.move(grub_backup, grub_file)
            return False                    
        except Exception as e:
            # 恢复备份
            shutil.move(grub_backup, grub_file)
            raise
    
    def run_command(self, command: List[str], sudo: bool = False) -> Tuple[bool, str]:
        '''
        执行系统命令并返回结果
        :param: command: 要执行的命令列表
        :param: sudo: 是否使用sudo执行(默认True)
        :returns Tuple[bool, str]: (是否成功, 输出内容)
        '''

        try:
            if isinstance(command,str):
                full_cmd = 'sudo ' + command if sudo else command
                self.logger.debug('full_cmd: {}'.format(full_cmd))
                result = subprocess.run(
                    full_cmd,
                    check=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    shell=True,
                    # text=True
                    universal_newlines=True
                )
            else:
                full_cmd = ['sudo'] + command if sudo else command
                self.logger.debug('full_cmd: {}'.format(full_cmd))
                result = subprocess.run(
                    full_cmd,
                    check=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    # text=True
                    universal_newlines=True
                )
            return True, result.stdout.strip()
        except subprocess.CalledProcessError as e:
            return False, e.stderr.strip()
        except FileNotFoundError as e:
            raise

    def backup_file(self, src_filepath:str, backup_filename:str = None, backup_dir:str = None, suffix=None) :
        ''' 
        创建配置文件备份
        :param file: 源文件的绝对路径
        :param backup_dir: 备份到的路径
        :returns str: 备份文件的绝对路径
        '''
        try:
            if backup_dir is None:
                backup_dir = self.backup_dir
            bakcup_path = Path(backup_dir)
            bakcup_path.mkdir(exist_ok=True,parents=True)
            
            if backup_filename is None:
                backup_filename = os.path.basename(src_filepath)
                
            if suffix is None:
                suffix = datetime.now().strftime("%Y%m%d%H%M%S")

            bakcup_path = bakcup_path / "{}.{}".format(backup_filename, suffix)
         
            shutil.copy2(src_filepath, str(bakcup_path))
            
            meta = os.stat(src_filepath)
            self.logger.debug("meta: {}".format(meta))
            
            os.lchown(str(bakcup_path), meta.st_uid, meta.st_gid)
            # os.chmod(src_filepath, meta.st_mode)
            # os.utime(src_filepath, (meta.st_atime, meta.st_mtime))
            
            self.logger.info("已创建备份: {}".format(bakcup_path))
            
            return str(bakcup_path)
        except Exception as e:
            # self.logger.error("Backup failed: {}".format(str(e)))
            # return None
            raise
    
    def restore_file(self, backup_filepath: str, src_filepath: str) -> bool:
        '''
        恢复文件
        :param src_filepath: 要恢复的文件的绝对路径
        :param backup_filepath: 备份文件的绝对路径
        :returns bool: 是否恢复成功
        '''
        if not os.path.exists(backup_filepath):
            self.logger.info('备份文件 {} 不存在'.format(backup_filepath))
            return  False
        if not os.path.exists(src_filepath):
            dir = os.path.dirname(src_filepath)
            Path(dir).mkdir(parents=True, exist_ok=True)

        try:
            with open(backup_filepath, 'r') as src, open(src_filepath, 'w+') as dst:
                dst.write(src.read())
            meta = os.stat(backup_filepath)
            os.lchown(src_filepath, meta.st_uid, meta.st_gid)
            os.chmod(src_filepath, meta.st_mode)
            os.utime(src_filepath, (meta.st_atime, meta.st_mtime))
            self.logger.info("已恢复文件: {}".format(src_filepath))
            return True
        except Exception as e:
            # print("Restore failed: {}".format(str(e)))
            # return False
            raise

    def read_pam_config2dict(self, pam_config_file: str, pam_module: str) -> dict:
        '''
        从 /etc/pam.d/common-auth 类似格式的配置文件中读取配置
        :param pam_config_file: 文件绝对路径
        :param pam_module: 模块名称，如 pam_unix
        :returns: 返回 {'__append': '', '__module': 'pam_unix.so', k=v}
        '''
        if not pam_module.endswith('.so'): pam_module += '.so'
        param_dict = {}
        if os.path.exists(pam_config_file):
            with open(pam_config_file, 'r') as f:
                for line in f:
                    line = line.strip()
                    if line.startswith('#') or not line:
                        continue
                    pam_pattern = re.compile(
                                                r'^\s*'                          # 行首可能存在的空白
                                                r'(?P<type>auth|account|session|password)\s+'  # 类型 (捕获组)
                                                r'(?P<control>\[[^\]]+\]|required|requisite|sufficient|optional)\s+'# 控制标志 (方括号或单词)
                                                r'(?P<module>{0})'           # 模块名 (必须以 .so 结尾)
                                                r'(\s+(?P<args>.+))?'            # 可选参数 (捕获组)
                                                r'\s*$'.format(pam_module)                         # 行尾空白
                                            , re.IGNORECASE)  # 忽略大小写
                    match = pam_pattern.match(line)
                    # match=re.match(r'^\s*(.*)\s+({0})\s+(.*)$'.format(pam_module), line, re.IGNORECASE)

                    if match:
                        self.logger.debug(match.groupdict())
                        param_dict['__append'] = match.group('type') + '\t' + match.group('control')
                        param_dict['__module'] = match.group('module')
                        params = match.group('args') or {}
                        for param in params.split():
                            if '=' in param:
                                key, value = param.split('=', 1)
                                param_dict.update({ key : value})
                            else:
                                param_dict.update({param : True})
                        break
        self.logger.debug(param_dict)
        return param_dict

    def write_dict2pam_config(self, pam_file:str, value_list:dict, pam_module: str = None, is_remove = False, is_tail=False):
        '''
        把 {'__append': '', '__module': 'pam_unix.so', k=v} 写入到 /etc/pam.d/common-auth 类似格式的配置文件中
        :param pam_file: 文件绝对路径
        :param value_list: 待写入的参数字典
        :returns: bool
        '''
                
        if not os.path.exists(pam_file):
            self.logger.error("未找到PAM配置文件")
            return False
        elif not value_list:
            self.logger.warning('提供的更新内容为空')
            return False
        
        if not pam_module :
            pam_module = value_list.get('__module')
        if not pam_module :
            self.logger.warning('新值中未找到模块名称，不符合PAM配置文件格式')
            return False    
        if not pam_module.endswith('.so'): pam_module += '.so'
    
        temp_file = None
        try:
            value_dict ={}
            for k,v in value_list.items():
                if k.startswith('__'): continue
                if isinstance(v,dict):
                    v = v.get('default')
                value_dict.update({k:v})
            self.logger.debug(value_list)
            
            value_string = ''
            self.logger.debug('== value_dict = {}'.format(value_dict))
            for k,v in value_dict.items():
                if isinstance(v,bool): value_string += '{} '.format(k) if v else ''
                else : value_string += '{}={} '.format(k,v)
            self.logger.debug(value_string)
            value_line = value_list['__append'] + '\t' + pam_module + '\t' + value_string + '\n'
            self.logger.debug(value_line)    
                        
            with open(pam_file, 'r') as f:
                old_lines = f.readlines()
                
            new_lines = []
            pam_found = False
            for line in old_lines:
                if line.startswith('#'): 
                    new_lines.append(line)
                    continue
                if re.search(r'{}'.format(pam_module), line):
                    pam_found = True
                    if is_remove: continue
                    line = value_line
                new_lines.append(line)
            if not pam_found and not is_remove:
                self.logger.info("未找到现有配置，将添加新规则")
                if is_tail:
                    new_lines.append(value_line)
                else:
                    new_lines.insert(0, value_line)
        
            # 写入新配置
            with tempfile.NamedTemporaryFile('w', delete=False) as tmp:
                temp_file = Path(tmp.name)
                self.logger.debug(temp_file)
                tmp.writelines(new_lines)
            
            # 备份原文件
            # self.backup_file(pam_file)
            # 替换原文件
            shutil.move(str(temp_file), str(pam_file))     
            self.logger.info("成功更新 {}".format(pam_file))
            return True
            
        except Exception as e:
            # self.logger.info("修改失败: {}".format(e))
            # return False
            raise
        finally:
            if temp_file and temp_file.exists():
                temp_file.unlink()

    def read_line_config2dict(self, config_file: str, separator:str) -> dict:
            '''
            从以行为单位的文件中读取配置
            :param config_file: 文件绝对路径
            :param separator: 分隔符
            :return: 返回 {'__separator': '', k:v}
            '''
            param_dict = {'__separator':separator}
            if os.path.exists(config_file):
                with open(config_file, 'r') as f:
                    for line in f:
                        if line.strip().startswith('#') or not line.strip():
                            continue
                        # match = re.match(r'^\s*(\S+)\s*{}\s*(\S+).*$'.format(separator), line)
                        # if match:
                        #     key = match.group(1)
                        #     value = match.group(2)
                        if separator == ' ':
                            result = line.split(maxsplit=1)
                        else:
                            result = line.split(separator, maxsplit=1)
                        key = result[0]
                        value = result[1] if len(result) > 1 else ' '
                        param_dict.update({ key.strip() : value.split(' ',1)[0].strip() })
                        
            self.logger.debug(param_dict)
            return param_dict

    def write_dict2line_config(self, config_file:str, values:dict,  separator:str = None):
        
        if not os.path.exists(config_file):
            self.logger.error("未找到配置文件")
            return False
        elif not values:
            self.logger.warning('提供的更新内容为空')
            return False
        separator = separator or values.get('__separator') 
        value_dict = { k:v for k,v in values.items() if not k.startswith('__') } 
        value_dict_copy = { k:v for k,v in values.items() if not k.startswith('__') }     
    
        temp_file = None
        new_lines = []
        try:
            with open(config_file, 'r') as f:
                old_lines = f.readlines()

            for line in old_lines:
                if line.startswith('#'): 
                    new_lines.append(line)
                    continue
                for key,value in  value_dict_copy.items():
                    if key in line:
                        line = key + separator + str(value) + '\n'
                        value_dict.pop(key)
                new_lines.append(line)
            
            # 如果没有找到相关配置，则添加
            if value_dict:
                for key,value in value_dict.items():
                    line = key + separator + str(value) + '\n'
                    new_lines.append(line)
                value_dict.clear()
                
            # 写入新配置
            with tempfile.NamedTemporaryFile('w', delete=False) as tmp:
                temp_file = Path(tmp.name)
                self.logger.debug(temp_file)
                tmp.writelines(new_lines)
            
            # 备份原文件
            # self.backup_file(config_file)
            # 替换原文件
            shutil.move(str(temp_file), str(config_file))     
            self.logger.info("成功更新 {}".format(config_file))
            return True
            
        except Exception as e:
            self.logger.info("修改失败: {}".format(e))
            return False
        finally:
            if temp_file and temp_file.exists():
                temp_file.unlink()

    def read_env(self, env_name: str):
        '''
        从 /etc/profile 中读取环境变量
        returns: str: 环境变量的值
        '''
        # value = os.environ[env_name]
        value = ''
        with open('/etc/profile', 'r') as f:
            for line in f:
                if line.startswith('#'): 
                    continue
                env_match = re.match(r'.*{}\s*=\s*(\S+).*'.format(env_name),line)
                if env_match :
                    value = env_match.group(1)
                    break
        return value

    def write_env(self, env_name: str, env_value: str):
        new_lines=[]
        is_found = False
        with open('/etc/profile', 'r') as f :
            for line in f:
                if line.startswith('#'): 
                    new_lines.append(line)
                    continue
                if re.match(r'.*{}\s*=\s*(\S+).*'.format(env_name),line):
                    line = 'export {}={}\n'.format(env_name,env_value)
                    if is_found:
                        continue #去重
                    else:
                        is_found = True
                new_lines.append(line)
            if not is_found:
                new_lines.append('export {}={}\n'.format(env_name,env_value))
        # 写入新配置
        with open('/etc/profile', 'w') as f :
            f.writelines(new_lines)

    
class Tally2Config(SecBase):
    def __init__(self):
        super().__init__()
        self.config_files = [
                            '/etc/pam.d/system-auth',
                            '/etc/pam.d/password-auth',
                            '/etc/pam.d/common-auth'  # 对于Debian/Ubuntu
                            ]
        self.config = { 'deny': 3 ,
                        'unlock_time': 300 ,
                        '__append': 'auth   requisite',
                        '__module': 'pam_tally2.so' 
                      }
        
        # 每个配置项格式: "key" : ["label", "type", "values",  "default_value"]   
        self.ui_config = OrderedDict([
            ( "deny", ("允许密码连续错误次数", "scale", (1,10,1) ,3 ) ),
            ( "unlock_time", ("密码错误锁定时间(秒)", "scale", (60,1800,60) ,180) ),
        ])
        
        for file in self.config_files:
            self.logger.debug(file)
            if os.path.exists(file):
                self.config_file=file
                break

    def write_config(self, values:dict=None):
        
        deny = self.config['deny']
        unlock_time = self.config['unlock_time']
        if values:
            deny=values.get('deny',deny)
            unlock_time=values.get('unlock_time',unlock_time)
           
        self.logger.info("正在修改文件: {}".format(self.config_file))  
        
        # 备份原文件
        # self.backup_file(self.config_file)
        
        # 读取并修改内容
        with open(self.config_file, 'r') as f:
            lines = f.readlines()
        
        new_lines = []
        pam_found = False
        
        for line in lines:
            if line.startswith('#'): 
                new_lines.append(line)
                continue
            # 查找包含pam_tally2或pam_faillock的行
            if re.search(r'pam_(tally2|faillock|unix)\.so', line):
                pam_found = True
                if 'pam_tally2.so' in line:
                    # 更新pam_tally2配置
                    line = re.sub(
                        r'(auth\s+required\s+pam_tally2\.so\s+).*',
                        r'\1deny={} onerr=fail unlock_time={} '.format(deny,unlock_time),
                        line
                    )
                    if 'even_deny_root' not in line:
                        line += ' even_deny_root'
                elif 'pam_faillock.so' in line:
                    # 更新pam_faillock配置
                    line = re.sub(
                        r'(auth\s+required\s+pam_faillock\.so\s+).*',
                        r'\1preauth deny={} unlock_time={}'.format(deny,unlock_time),
                        line
                    )
                else:
                    line=re.sub(r'\b(deny|unlock_time)=\S+','',line).strip()
                    line += ' deny={} unlock_time={}\n'.format(deny,unlock_time)
            new_lines.append(line)
        
        # 如果没有找到相关配置，则添加
        if not pam_found:
            print("未找到现有配置，将添加新规则")
            new_lines.insert(0, "auth   required   pam_tally2.so even_deny_root deny={} unlock_time={}\n".format(deny,unlock_time))
            
        # 写入新配置
        with open(self.config_file, 'w') as f:
            f.writelines(new_lines)
        
        self.logger.info("成功更新 {}".format(self.config_file))

    def read_config(self) -> dict :
        """
        读取密码错误次数和锁定时间
        """
        deny = unlock_time = ''

        self.logger.debug("正在读文件: {}".format(self.config_file))

        # 读取并修改内容
        with open(self.config_file, 'r') as f:
        
            for line in f:
                if line.startswith('#'): 
                    continue
                # 查找包含pam_tally2或pam_faillock的行
                if re.search(r'pam_(tally2|faillock|unix)\.so', line):
                    deny_match = re.match(r'.*deny=(\d+).*',line)
                    if deny_match : deny = deny_match.group(1)
                    unlock_time_match = re.match(r'.*unlock_time=(\d+).*',line)
                    if unlock_time_match : unlock_time = unlock_time_match.group(1)
        return { 'deny' : deny , 'unlock_time': unlock_time }

        
class PwqualityConfig(SecBase):
    def __init__(self):
        self.libname = 'pwquality'
        super().__init__()
        self.config_files = ['/etc/security/pwquality.conf']
        self.config ={ 
                    'minlen': 8 , 
                    'minclass': 1 , 
                    'maxrepeat': 0 ,  
                    'dcredit': -1 ,   # 若 N 大于或等于 0，则此数值代表密码中的数字对密码长度 minlen 的最大额外贡献值。默认为 0，代表数字没有额外贡献值。
                    'ocredit': -1,    # 若 N 小于 0，則此数值代表密码中至少要包含多少个数字。
                    'retry': 3,  
                    '__append': 'password   requisite',
                    '__module': 'pam_pwquality.so' 
                    }
        
        self.ui_config = OrderedDict([
            ( "minlen", ("最小密码长度", "scale", (1,32,1) ,8) ),
            ( "minclass", ("最少字符种类", "combo", ["1", "2", "3", "4"], "3") ),
            # ( "ucredit", ("必须包含大写字母", "check", False) ),
            # ( "lcredit", ("必须包含小写字母", "check", True) ),
            # ( "dcredit", ("必须包含数字", "check", False) ),
            # ( "ocredit", ("必须包含其他字符", "check", False) ),
        ])
        for file in self.config_files:
            if os.path.exists(file):
                self.config_file=file
                break
            
    def read_config(self) -> Dict:
        """读取当前配置"""
        config = self.read_line_config2dict(self.config_file, '=')
        return config

    def write_config(self, updates: Dict):
        if updates:
            self.write_dict2line_config(self.config_file,updates,'=')
        else:
            self.write_dict2line_config(self.config_file, self.config, '=')
    

class CracklibConfig(SecBase):
    '''  https://manpages.debian.org/stretch/libpam-cracklib/pam_cracklib.8.en.html '''
    def __init__(self):
        super().__init__()
        self.config_file =  [ '/etc/pam.d/common-password', '/etc/pam.d/system-auth']
        self.config = {
                    'minlen': {'default': 8, 'range': [3,10], "type": "scale", "msg": "最小长度"}, 
                    'minclass': {'default': 1, 'range': [3,10], "type": "scale", "msg": "必须包含的字符种类"}, 
                    'maxrepeat': {'default': 0, 'range': [3,10], "type": "scale", "msg": "允许重复字符的数量"},  
                    'dcredit': {'default': -1, 'range': [-3, 10], "type": "scale", "msg": "数字的数量"},   
                    'ucredit': {'default': -1, 'range': [-3, 10], "type": "scale", "msg": "大写字母的数量"},  
                    'lcredit': {'default': -1, 'range': [-3, 10],  "type": "scale", "msg": "小写字母的数量"},  
                    'ocredit': {'default': -1, 'range': [-3, 10], "type": "scale", "msg": "特殊字符的数量"},   
                    'retry': {'default': 3, 'range': [1, 10], "type": "scale", "msg": "密码错误尝试次数"},  
                    'difok': {'default': 3, 'range': [1, 10], "type": "scale", "msg": "与旧密码可以相同的次数"}, 
                    'remember': {'default': 5, 'range': [1, 10], "type": "scale", "msg": "密码历史记录"},
                    '__append': 'password   requisite',
                    '__module': 'pam_cracklib.so'
                }
        
        self.ui_config = OrderedDict([
            ( "minlen", ("最小密码长度", "scale", (1,32,1) ,8) ),
            ( "minclass", ("最少字符种类", "combo", ["1", "2", "3", "4"], "3") ),
            # ( "ucredit", ("必须包含大写字母", "check", False) ),
            # ( "lcredit", ("必须包含小写字母", "check", True) ),
            # ( "dcredit", ("必须包含数字", "check", False) ),
            # ( "ocredit", ("必须包含其他字符", "check", False) ),
        ])
   
        for file in self.config_file:
            print(file)
            if os.path.exists(file):
                self.config_file=file
                break
        

    def read_config(self) -> Dict:
        """读取并解析配置文件"""
        config = self.read_pam_config2dict(self.config_file, self.config['__module'])
        return config

    def write_config(self, updates: Dict):
        """写入配置更新"""
        updates['__append'] = self.config['__append']
        if updates:
            self.write_dict2pam_config(self.config_file, updates , self.config['__module'])
        else:
            self.write_dict2pam_config(self.config_file, self.config)

    
class LoginDefsEditor(SecBase):
    
    def __init__(self):
        super().__init__()
        self.config_file = '/etc/login.defs'
        self.config = {
                    'PASS_MAX_DAYS': {'default': 90, 'range': [1,100], "type": "scale", "msg": "密码最大有效期90天"}, 
                    'PASS_MIN_DAYS': {'default': 1, 'range': [3,10], "type": "scale", "msg": "密码最短使用7天"}, 
                    'PASS_WARN_AGE': {'default': 0, 'range': [3,10], "type": "scale", "msg": "密码过期前14天警告"},  
                    'UMASK': {'default': -1, 'range': [-3, 10], "type": "scale", "msg": "022"},   
                }
        
        self.ui_config = OrderedDict([
            ( "PASS_MAX_DAYS", ("新用户密码最长有效期(天)", "combo", ["7", "30", "90", "180", "99999"], "99999") ), # 永久=99999 
            ( "PASS_MIN_DAYS", ("新用户密码最短有效期(天)", "scale", (0,99,1) ,0) ),
            ( "PASS_WARN_AGE", ("密码过期提前告警(天)", "scale", (1,99,1) ,3) ),
        ])
  
  
    def read_config(self):
        config = self.read_line_config2dict(self.config_file, ' ')
        return config

    def write_config(self,values:dict):
        if values:
            self.write_dict2line_config(self.config_file, values ,' ')
        else:
            self.write_dict2line_config(self.config_file, self.config ,' ')



class AuthManager(SecBase):


    def __init__(self, config: dict=None):
        super().__init__()
        # 1.密码复杂和认证次数对象
        self.os_pam_list = self.get_pam_modules()
        self.logger.debug(self.os_pam_list)
        if 'pam_pwquality' in self.os_pam_list:
            self.password_pam = PwqualityConfig()
        elif 'pam_cracklib' in self.os_pam_list:
            self.password_pam = CracklibConfig()            
        else:
            self.password_pam = None
            self.logger.error('系统没有按照密码复杂的相关的 PAM 模块,需要安装 pam_pwquality 或 pam_cracklib !!!')
        # 新创建账号密码复杂对象
        self.login_defs = LoginDefsEditor()
        self.tally2_pam = Tally2Config()        
        self.system_config = {}
        self.config = {
                    # 'passwd_config'
                    'minlen': 3,
                    'dcredit': 0,
                    'ucredit': 0,
                    'lcredit': 0,
                    'ocredit': 0,
                    'minclass': 1,
                    'maxrepeat': 0,
                    'maxsequence': '0',
                    'maxclassrepeat': '0',
                    'dictcheck': '1', # 字典检查
                    'usercheck': '1',
                    'dictpath': '/var/cache/cracklib/cracklib_dict',
                    'retry': 5,
                    # 'tally2_config'
                    'deny': '3',
                    'unlock_time': '180',
                    # 'login_defs_config'
                    'UMASK': '022',
                    'PASS_MAX_DAYS': '90',
                    'PASS_MIN_DAYS': '7',
                    'PASS_WARN_AGE': '14'
                    }
        self.ui_config = { 'id' : 'auth', 
                           'category': '密码强度', 
                           'uis': OrderedDict([
                               *self.password_pam.ui_config.items(), 
                               *self.tally2_pam.ui_config.items() , 
                               *self.login_defs.ui_config.items() ,
                               ( "session_time", ("会话超时时间 TMOUT (秒)", "scale", (60,1800,60) ,300) ),
                            ])
                          }

            
    def get_prefer_config(self):
        return self.config

    def read_config(self):
        passwd_config = self.password_pam.read_config()
        self.logger.debug('== passwd_config = {}'.format(passwd_config))
        if passwd_config: self.system_config.update(passwd_config)

        tally2_config = self.tally2_pam.read_config()
        self.logger.debug('== tally2_config = {}'.format(tally2_config))
        if tally2_config: self.system_config.update(tally2_config)

        login_defs_config = self.login_defs.read_config()
        self.logger.debug('== login_defs_config = {}'.format(login_defs_config))
        if login_defs_config: self.system_config.update(login_defs_config)

        self.logger.debug('== config = {}'.format(self.system_config))
        return self.system_config

    def write_config(self,config=None):
        if config :
            passwd_keys = self.password_pam.config.keys()
            tally2_keys = self.tally2_pam.config.keys()
            login_keys= self.login_defs.config.keys()
            
            passwd_config = {}
            tally2_config = {}
            login_config = {}
            
            for key,value in config.items():
                if key in passwd_keys:
                    passwd_config.update( {key: value} )
                    continue
                if key in tally2_keys:
                    tally2_config.update( {key: value} )
                    continue
                if key in login_keys:
                    login_config.update( {key: value} )
                    continue
                if key == 'session_time':
                    self.write_env('TMOUT',value)

            
            self.logger.debug('== passwd_config = {}'.format(passwd_config))
            if passwd_config: self.password_pam.write_config(passwd_config)
        
            self.logger.debug('== tally2_config = {}'.format(tally2_config))
            if tally2_config: self.tally2_pam.write_config(tally2_config)

            self.logger.debug('== login_defs_config = {}'.format(login_config))
            if login_config: self.login_defs.write_config(login_config)
    
    def backup(self, suffix=None):
        password_file = self.backup_file(self.password_pam.config_file,suffix=suffix )
        tally_file = self.backup_file(self.tally2_pam.config_file,suffix=suffix )
        login_file = self.backup_file(self.login_defs.config_file,suffix=suffix )
        backup_info = { 'password_file' : password_file,
                 'tally_file': tally_file, 
                 'login_file': login_file
                }
        return { 'AuthManager': backup_info }
        
    def restore(self, config):
        password_file = config.get('password_file')
        tally_file = config.get('tally_file')
        login_file = config.get('login_file')
        if password_file:
            self.restore_file(password_file, self.password_pam.config_file)
        if tally_file:
            self.restore_file(tally_file, self.tally2_pam.config_file)
        if login_file:
            self.restore_file(login_file, self.login_defs.config_file)



class UserManager(SecBase):
    def __init__(self):
        """初始化用户管理器"""
        super().__init__()
        self.sudoers_file = "/etc/sudoers"
        self.sudo_log = "/var/log/sudo.log"
        self._change_fields = [
            'username', 'password', 'shell', 'groups', 'min_days',
            'max_days', 'warn_days', 'inactive_days', 'is_locked', 'expire_date'
        ]
        
        self.config  = {
            "sudo_logging": True,
            "sudo_users":[],
            "su_wheel": False,
            "su_users": [],
            "users": {}
        }
        
        self.system_config = {
            "sudo_logging": bool,
            "sudo_users":[],
            "su_wheel": bool,
            "su_users": [],
            "users": {}
        }
        
        self.ui_config ={
            'id': 'user',
            'category': '账号权限',
            'uis': OrderedDict([
                 ( "users", ("修改账号属性", "user_table") ),
                 ( "sudo_logging", ("是否开启 sudo 日志(/var/log/sudo)", "check", False) ),
                 ( "sudo_users", ("具有 sudo 权限的用户(,分割)", "entry", None) ),
                 ( "su_wheel", ("是否开启 su 切换用户限制", "check", False) ),
                 ( "su_users", ("具有 su 权限的用户(,分割)", "entry", None) ),
                 ( 'disable_0uid',  ("锁定 UID =0 的非 root 账号", "check", True) ),
            ]),
            'users_columns': OrderedDict([ ('username', '用户名'), ('uid', 'UID'),('gid', 'GID'), ('shell', 'shell'), ('is_locked','锁定'), 
                                          ('min_days', '最短有效期'), ('max_days','最长有效期'), 
                                           ('warn_days','警告时间'), ('inactive_days','宽限期'), ('expire_date','过期时间') ,('groups','组') ]),
            'users_combo_columns': {
                # 'username': [ user['username'] for user in self.get_all_users() ],
                'is_locked' : [ 'True', 'False' ]       
            },
            'users_data' : self.get_all_users()
        }

    def get_prefer_config(self):
        return self.config
    
    def read_config(self):
        self.system_config['sudo_logging'] = self.check_sudo_logging()
        self.system_config['sudo_users'] = []
        self.system_config['su_wheel'] = self.check_su_wheel()
        self.system_config['su_users'] = []
        users = self.get_all_users()
        self.system_config['users'] =users
        
        for user, _ in users.items():
            if self.check_sudo_access(user):
                self.system_config['sudo_users'].append(user)
            if self.check_su_access(user):
                self.system_config['su_users'].append(user)
        return self.system_config
        
    def write_config(self,config: dict=None):
        if config is None:
            config = self.config
                    
        # 这里必须放在第一条，否则会覆盖 su 和 sudo 的配置
        users= config.get('users',{})
        if users: self.set_all_users(users)
        
        # 更新 su 用户，未指定的用户删除 su 权限
        is_su_wheel = config['su_wheel']
        self.change_su_wheel(is_su_wheel)  
        if is_su_wheel:    
            self.ensure_group_exist('wheel',is_create=True)    
            users= config.get('su_users',[])
            if users and users[0] != '':
                self.manage_group_users('wheel', users, True, is_remove_old=True)
 
        sudo_logging = config.get('sudo_logging',False)
        self.manage_sudo_logging(sudo_logging)
        
        # 更新 sudo 用户，未指定的用户删除 sudo 权限
        users= config.get('sudo_users',[])
        if users and users[0] != '':
            self.ensure_group_exist('sudo',is_create=True)   
            self.manage_sudoers_user('%sudo', True)
            # for user in users:
            #     self.manage_group_users('sudo', users, True, is_remove_old=True)
            self.manage_sudo_users(users, True, True)

        if config.get('disable_0uid'):
            self.disable_0uid()
    
    
    def check_su_wheel(self):
        if self.read_pam_config2dict('/etc/pam.d/su','pam_wheel'):
            return True
        else:
            return False
        
    def change_su_wheel(self,do_enable: bool=True):
        use_uid = False
        if self.os_info['package_type'] == 'rpm':
            use_uid = True
        if do_enable:
            # 写在最后，否则在 v10 server 中 root 无法切用户
            self.write_dict2pam_config('/etc/pam.d/su',{'use_uid': use_uid, '__append': 'auth\trequired'}, 'pam_wheel',is_tail=True)
        else:
            self.write_dict2pam_config('/etc/pam.d/su',{'use_uid': use_uid, '__append': 'auth\trequired'}, 'pam_wheel', True)
        
        
    def _modify_sudoers(self, content: str) -> bool:
        """安全修改 sudoers 文件"""
        try:
            # 创建临时文件
            with tempfile.NamedTemporaryFile('w', delete=False) as tmp:
                tmp.write(content)
                tmp_path = tmp.name
            
            # 验证语法
            check_cmd = ['visudo', '-c', '-f', tmp_path]
            result,message = self.run_command(check_cmd)
            
            if not result:
                # os.remove(tmp_path)
                raise ValueError("sudoers 文件语法错误: {}".format(message))
            
            # 备份原文件
            # self.backup_file(self.sudoers_file)
            # 替换文件
            self.run_command(['cp', tmp_path, self.sudoers_file])
            os.remove(tmp_path)
            return True
        except Exception as e:
            print("修改 sudoers 文件出错: {}".format(str(e)))
            return False

    def manage_sudoers_user(self, username: str, enable: bool = True) -> bool:
        """启用/禁用用户 sudo 权限"""
        try:
            if not username: return False
            # 读取当前 sudoers 内容
            with open(self.sudoers_file, 'r') as f:
                lines = f.readlines()
            
            # 查找用户配置
            new_lines = []
            user_line = "{} ALL=(ALL:ALL) ALL".format(username)
            found = False
            
            sudoers_pattern = re.compile(r'^\s*({})\s+\S+\s*=\(\S+\)\s+.*$'.format(username))
            for line in lines:
                if sudoers_pattern.match(line):
                    if not enable:
                        continue
                    found = True
               
                new_lines.append(line)
            
            # 如果未找到则添加
            if enable and not found:
                new_lines.append("{}\n".format(user_line))
            
            # 写入修改
            return self._modify_sudoers(''.join(new_lines))
        
        except Exception as e:
            print("Error managing sudo access: {}".format(str(e)))
            return False
        
    def get_sudo_users(self):
        self.sudoers_users = []
        self.sudo_users = []
        
        sudoers_pattern = re.compile(r'^\s*(\S+)\s+\S+\s*=\(\S+\)\s+.*$')
        with open('/etc/sudoers') as f:
            for i in f:
                result = sudoers_pattern.match(i)
                if result:
                    user_or_group = result.group(1)
                    self.logger.info("{} 用户/组 : {}".format(user_or_group,result.group()))
                    if not user_or_group.startswith('%') and user_or_group != 'root':
                        self.sudoers_users.append(user_or_group)
        # if self.sudoers_users:
        #     self.sudo_users.extend(self.sudoers_users)
        sudo_group_users = self.get_group_users('sudo')
        if sudo_group_users:
            self.sudo_users.extend(sudo_group_users)
        return list(set(self.sudoers_users + self.sudo_users))
    
    
    def manage_sudo_users(self, users, enable: bool = True, is_remove_old=False) -> bool:
        if not users:
            return False
        # 先计算补集
        old_users = self.get_sudo_users()
        old_sudo_users = [ user for user in self.sudo_users if user not in users ] 
        old_sudoers_users = [ user for user in self.sudoers_users if user not in users ] 
        new_users = [ user for user in users if user not in old_users ]
        
        self.logger.info("删除 sudo 用户：{}".format(old_sudo_users))
        self.logger.info("删除 sudoers 用户：{}".format(old_sudoers_users))
        self.logger.info("添加 sudo 用户：{}".format(new_users))
        
        # 删除参数没有的用户
        if enable and is_remove_old:
            for username in old_sudoers_users:
                    self.manage_sudoers_user(username,False)
      
            self.manage_group_users('sudo', old_sudo_users, False)
        
                
        action = "-a" if enable else "-d"
        for username in users:
            success, output =  self.run_command(['gpasswd', action, username, 'sudo' ])
        return success
        

    def manage_group_users(self, group:str, users: str, enable: bool = True, is_remove_old=False) -> bool:
        """添加/删除用户组"""
        if not users:
            return False
        # 先计算补集
        old_users = self.get_group_users('wheel')
        old_users = [ user for user in old_users if user not in users ] 
        new_users = [ user for user in users if user not in old_users ]
        
        self.logger.info("{} 组删除用户：{}".format(group, old_users))
        self.logger.info("{} 组添加用户：{}".format(group, new_users))
        
        if enable and is_remove_old:
            # 从组中删除所有用后追加
            for username in old_users:
                success, output =  self.run_command(['gpasswd', '-d', username, group ])
                
        action = "-a" if enable else "-d"
        for username in new_users:
            success, output =  self.run_command(['gpasswd', action, username, group ])
            
        return success
    
    
    def get_group_users(self, group:str) -> list:
        try:
            # 尝试获取组信息
            users = grp.getgrnam(group).gr_mem
            self.logger.info("{}  用户有 {}".format(group, users))
            return users
        except KeyError: 
            self.logger.info("{} 组不存在".format(group))
            return []
        
    
    def ensure_group_exist(self, group, is_create=False) -> bool:
        '''
        判断一个组是否存在，不存在就创建
        '''
        try:
            # 尝试获取组信息
            grp.getgrnam(group)
            self.logger.info("{} 已经存在".format(group))
            return True
        except KeyError: 
            self.logger.info("{} 组不存在".format(group))
            if is_create:          
                # 使用 groupadd 命令创建组
                result, output = self.run_command( ['sudo', 'groupadd', group ] )
                if result:
                    self.logger.info("{} 组创建成功".format(output))
                    return True
                self.logger.info("{} 组创建失败".format(output))
            return False


    def manage_sudo_logging(self, enable: bool = True) -> bool:
        """配置 sudo 日志记录"""
        try:
            with open(self.sudoers_file, 'r') as f:
                lines = f.readlines()
            
            new_lines = []
            log_entry = 'Defaults logfile="/var/log/sudo.log"'
            found = False
            
            for line in lines:
                stripped = line.strip()
                if stripped.startswith("Defaults logfile="):
                    if enable:
                        new_lines.append("{}\n".format(log_entry))
                    found = True
                else:
                    new_lines.append(line)
            
            if enable and not found:
                new_lines.append("{}\n".format(log_entry))
            
            return self._modify_sudoers(''.join(new_lines))
        
        except Exception as e:
            print("Error managing sudo logging: {}".format(str(e)))
            return False

    def check_sudo_access(self, username: str) -> bool:
        """检查用户是否有 sudo 权限"""
        success, output =  self.run_command(['env', 'LANG=en', 'sudo', '-l', '-U', username])
        return "may run the following commands" in output

    def check_su_access(self, username: str) -> bool:
        """检查用户是否有 su 权限"""
        success, output =  self.run_command(['groups', username])
        return "wheel" in output.split()

    def check_sudo_logging(self) -> bool:
        """检查是否启用了 sudo 日志"""
        try:
            with open(self.sudoers_file, 'r') as f:
                content = f.read()
                return 'logfile="/var/log/sudo.log"' in content
        except:
            return False

    def set_all_users(self, user_infos: List ):
        if user_infos is not None:
            for user in user_infos:
                # print(user, info)
                user = {k:v for k,v in user.items() if k in self._change_fields}
                # print(user)
                if user :
                    self.modify_user(**user)
        
    def get_all_users(self) -> Dict:
        """
        获取所有用户及其详细信息
        返回: 包含每个用户信息的字典列表
            [
                {
                    {  'username': 'root','uid': 0, 'gid': 0, 'groups': ['root'], 'home': '/root', 'shell': '/bin/bash', 'is_locked': False, 'can_login': True, 
                       'password_info': {'last_change': '2024-11-09', 'min_days': 0, 'max_days': 99999, 'warn_days': 7, 'inactive_days': None, 'expire_date': None},
                       'gecos': 'root'
                    },
                }
            ]
        """
        users = []
        
        for user in pwd.getpwall():
            shadow = spwd.getspnam(user.pw_name)
            can_login = self._can_user_login(user.pw_shell, shadow.sp_pwdp or '')
            if not can_login: continue
            # 跳过系统用户(通常UID < 1000)，但保留root
            if user.pw_uid >= 1000 or user.pw_uid == 0:
                groups = self._get_user_groups(user.pw_name, user.pw_gid)
                users.append(
                    {
                        'username' : user.pw_name,
                        'uid': user.pw_uid,
                        'gid': user.pw_gid,
                        'groups': groups,
                        'home': user.pw_dir,
                        'shell': user.pw_shell,
                        'is_locked': shadow.sp_pwdp.startswith(('!', '*')),
                        'can_login': can_login,
                        **self._parse_shadow_info(shadow),
                        'gecos': user.pw_gecos
                    }
                )
               
        return users
    
    def _get_user_groups(self, username: str, primary_gid: int) -> List[str]:
        """获取用户所属的所有组"""
        groups = [grp.getgrgid(primary_gid).gr_name]
        for group in grp.getgrall():
            if username in group.gr_mem and group.gr_name not in groups:
                groups.append(group.gr_name)
        return groups
    
    def _can_user_login(self, shell: str, password: str) -> bool:
        """检查用户是否可以登录"""
        if shell in ('/bin/false', '/usr/sbin/nologin', '/sbin/nologin'):
            return False
        # if password.startswith(('!', '*')):
        #     return False
        return True
    
    def _parse_shadow_info(self, shadow: spwd.struct_spwd) -> Dict[str, Union[int, str, None]]:
        """解析shadow信息"""
        return {
            'last_change': self._days_to_date(shadow.sp_lstchg),
            'min_days': shadow.sp_min,
            'max_days': shadow.sp_max,
            'warn_days': shadow.sp_warn,
            'inactive_days': shadow.sp_inact if shadow.sp_inact != -1 else 'Never',
            'expire_date': self._days_to_date(shadow.sp_expire) if shadow.sp_expire != -1 else "Never"
        }
    
    def _days_to_date(self, days: int) -> str:
        """将天数转换为日期字符串"""
        return (datetime(1970, 1, 1) + timedelta(days=days)).strftime('%Y-%m-%d')
    
    def _date_to_days(self, date_str: str) -> int:
        """将日期字符串转换为天数(自1970-01-01)"""
        date_obj = datetime.strptime(date_str, '%Y-%m-%d')
        return (date_obj - datetime(1970, 1, 1)).days
    
    def modify_user(
        self,
        username: str,
        password: Optional[str] = None,
        shell: Optional[str] = None,
        # home: Optional[str] = None,
        groups: Optional[List[str]] = None,
        is_locked: Optional[bool] = None,
        min_days: Optional[bool] = None,
        max_days: Optional[bool] = None,
        warn_days: Optional[str] = None,
        expire_date: Optional[str] = None,
        inactive_days: Optional[int] = None
    ) -> bool:
        """
        修改用户属性
        参数:
            username: 要修改的用户名
            password: 新密码(设为空字符串表示清除密码)
            shell: 新shell路径
            home: 新家目录路径
            groups: 用户所属的新组列表
            lock: True锁定用户，False解锁用户
            min_days: 最小天数,将两次改变密码之间相距的最小天数设为“最小天数”
            max_days: 最大天数
            warn_days: 将过期警告天数设为“警告天数”
            inactive_days: 密码过期后多少天账户被禁用
        返回:
            bool: 修改是否成功
        """
        try:
            # 修改基本属性
            if shell is not None:
                self.run_command(['usermod', '-s', shell, username])
            
            # if home is not None:
            #     self.run_command(['usermod', '-d', home, username])
            
            # 修改组
            if groups is not None:
                if isinstance(groups,list): groups=','.join(groups)
                self.run_command(['usermod', '-G', groups , username])
            
            # 锁定/解锁账户
            if is_locked is not None:
                if str(is_locked).upper() == 'TRUE' : 
                    is_locked = True
                else:
                    is_locked = False
                action = '-L' if is_locked else '-U'
                self.run_command(['usermod', action, username])
            
            # 修改密码相关属性
            shadow_cmd = ['chage']
            if expire_date is not None:
                shadow_cmd.extend(['-E', expire_date])
            if warn_days is not None:
                shadow_cmd.extend(['-W', str(warn_days)])
            if inactive_days is not None:
                shadow_cmd.extend(['-I', str(inactive_days)])
            if min_days is not None:
                shadow_cmd.extend(['-m', str(min_days)])
            if max_days is not None:
                shadow_cmd.extend(['-M', str(max_days)])
            if len(shadow_cmd) > 1:
                shadow_cmd.append(username)
                self.run_command(shadow_cmd)
            
            # 修改密码
            if password is not None:
                if password == "":
                    # 清除密码(禁用密码登录)
                    self.run_command(['passwd', '-d', username])
                else:
                    # 使用chpasswd设置密码
                    proc = subprocess.Popen(
                        ['chpasswd'],
                        stdin=subprocess.PIPE,
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE,
                        text=True
                    )
                    proc.communicate(input="{}:{}".format(username, password))
                    if proc.returncode != 0:
                        raise RuntimeError("Failed to set password")
            
            return True
        except Exception as e:
            print("Error modifying user {}: {}".format(username,str(e)))
            raise
            return False
    
    def create_user(
        self,
        username: str,
        password: str,
        shell: str = "/bin/bash",
        home: Optional[str] = None,
        groups: Optional[List[str]] = None,
        system_user: bool = False,
        create_home: bool = True
    ) -> bool:
        """
        创建新用户
        参数:
            username: 新用户名
            password: 密码
            shell: 默认shell
            home: 家目录路径(默认/home/username)
            groups: 用户所属的组
            system_user: 是否创建为系统用户
            create_home: 是否创建家目录
        返回:
            bool: 创建是否成功
        """
        try:
            cmd = ['useradd']
            if system_user:
                cmd.append('-r')
            if not create_home:
                cmd.append('-M')
            else:
                cmd.append('-m')
            
            if shell:
                cmd.extend(['-s', shell])
            
            if home:
                cmd.extend(['-d', home])
            
            if groups:
                cmd.extend(['-G', ','.join(groups)])
            
            cmd.append(username)
            self.run_command(cmd)
            
            # 设置密码
            if password:
                self.modify_user(username, password=password)
            
            return True
        except Exception as e:
            print("Error creating user {}: {}".format(username,str(e)))
            return False
    
    def delete_user(self, username: str, remove_home: bool = True) -> bool:
        """
        删除用户
        参数:
            username: 要删除的用户名
            remove_home: 是否同时删除家目录
        返回:
            bool: 删除是否成功
        """
        try:
            cmd = ['userdel']
            if remove_home:
                cmd.append('-r')
            cmd.append(username)
            self.run_command(cmd)
            return True
        except Exception as e:
            print("Error deleting user {}: {}".format(username, str(e)))
            return False

    def disable_0uid(self):
        users = self.get_all_users()
        for user in users:
            username = user.get('username')
            uid = user.get('uid')
            if uid == 0 and username != 'root':
                self.logger.info('找到 {} 账号 UID=0，需要锁定'.format(username))
                self.run_command(['usermod', '-L', username])


    def backup(self, suffix=None):
        sudoers_file = self.backup_file(self.sudoers_file,suffix=suffix )
        passwd_file  = self.backup_file('/etc/passwd',suffix=suffix )
        shadow_file  = self.backup_file('/etc/shadow',suffix=suffix )
        su_file= self.backup_file('/etc/pam.d/su',suffix=suffix )
        backup_info = { 'sudoers_file' : sudoers_file,
                 'passwd_file': passwd_file, 
                 'shadow_file': shadow_file,
                 'su_file': su_file
                }
        return { 'UserManager': backup_info }
        
    def restore(self, config):
        sudoers_file = config.get('sudoers_file')
        passwd_file = config.get('passwd_file')
        shadow_file = config.get('shadow_file')
        su_file = config.get('su_file')
        if sudoers_file:
            self.restore_file(sudoers_file, self.sudoers_file)
        if passwd_file:
            self.restore_file(passwd_file, '/etc/passwd')
        if shadow_file:
            self.restore_file(shadow_file, '/etc/shadow')
        if su_file:
            self.restore_file(su_file, '/etc/pam.d/su')




class SSHManager(SecBase):
    def __init__(self):
        super().__init__()
        self.sshd_config_path = "/etc/ssh/sshd_config"
        self.ssh_config_path = "/etc/ssh/ssh_config"
        self.config = {
            "Port": 22,
            "MaxAuthTries": 3,
            "PermitRootLogin": "no",
            "PasswordAuthentication": "yes",
            "PermitEmptyPasswords": "no",
            "LoginGraceTime": 60,
            "AllowUsers": "",
            "Ciphers": "",
            "KexAlgorithms": "curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256",
        }
        
        self.ui_config = {
            'id': 'ssh',
            'category': 'SSH 配置',
            'uis': OrderedDict([
                ( "Port", ("SSH 监听端口", "spin", 22) ),
                ( "PermitRootLogin", ("SSH 是否允许 root 登录", "check", False) ),
                ( "PasswordAuthentication", ("允许密码登录", "check", True) ),
                ( "MaxAuthTries", ("密码最大重试次数",  "scale", (1,10,1) ,3) ), 
            ])
        }
        
    def _update_config(self, key: str, value: str, comment: str = "") -> bool:
        """
        更新 SSH 配置项
        参数:
            key: 配置项名称
            value: 配置值
            comment: 可选注释
        返回:
            bool: 是否更新成功
        """

        if not os.path.exists(self.sshd_config_path):
            return False
        
        try:
            with open(self.sshd_config_path, 'r') as f:
                lines = f.readlines()
            
            # 查找现有配置项
            found = False
            new_lines = []
            for line in lines:
                if re.match('^\s*{}\s+'.format(key), line, re.IGNORECASE):
                    new_lines.append("{} {}\t\t# {}\n".format(key,value,comment) if comment else "{key} {value}\n".format(key, value))
                    found = True
                else:
                    new_lines.append(line)
            
            # 如果没找到则追加
            if not found:
                new_lines.append("{} {}\t\t# {}\n".format(key,value,comment) if comment else "{} {}\n".format(key,value))
            
            with open(self.sshd_config_path, 'w') as f:
                f.writelines(new_lines)
            
            return True
        except Exception as e:
            print("Update config failed: {}".format(str(e)))
            return False
    
    def change_port(self, new_port: int) -> bool:
        """
        修改 SSH 端口
        参数:
            new_port: 新端口号(1-65535)
        返回:
            bool: 是否修改成功
        """
        if not 1 <= new_port <= 65535:
            print("Invalid port number")
            return False
        
        return self._update_config("Port", str(new_port), "Custom SSH port for security")
    
    def set_max_auth_tries(self, max_attempts: int = 3) -> bool:
        """
        设置最大密码尝试次数
        参数:
            max_attempts: 最大尝试次数(默认3)
        返回:
            bool: 是否设置成功
        """
        if max_attempts < 1:
            print("Invalid max attempts value")
            return False
        
        return self._update_config("MaxAuthTries", str(max_attempts), "Limit password attempts")
    
    def configure_root_login(self, permit: bool = False) -> bool:
        """
        配置是否允许 root 登录
        参数:
            permit: 是否允许(默认False)
        返回:
            bool: 是否配置成功
        """
        value = "prohibit-password" if permit else "no"
        return self._update_config("PermitRootLogin", value, "Root login permission")
    
    def disable_password_auth(self, disable: bool = True) -> bool:
        """
        禁用密码认证(仅允许密钥认证)
        参数:
            disable: 是否禁用(默认True)
        返回:
            bool: 是否设置成功
        """
        value = "no" if disable else "yes"
        return self._update_config("PasswordAuthentication", value, "Password-based authentication")
    
    def disable_empty_passwords(self) -> bool:
        """
        禁用空密码
        返回:
            bool: 是否设置成功
        """
        return self._update_config("PermitEmptyPasswords", "no", "Prevent empty passwords")
    
    def set_login_grace_time(self, seconds: int = 60) -> bool:
        """
        设置登录宽限时间
        参数:
            seconds: 宽限时间(秒, 默认60) 
        返回:
            bool: 是否设置成功
        """
        if seconds < 10:
            print("Login grace time too short")
            return False
        
        return self._update_config("LoginGraceTime", str(seconds), "Time limit for authentication")
    
    def disable_weak_kex(self) -> bool:
        """
        禁用弱密钥交换算法
        返回:
            bool: 是否设置成功
        """
        return self._update_config(
            "KexAlgorithms", 
            "curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256",
            "Strong key exchange algorithms"
        )
    
    def disable_weak_ciphers(self) -> bool:
        """
        禁用弱加密算法
        返回:
            bool: 是否设置成功
        """
        return self._update_config(
            "Ciphers", 
            "chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr",
            "Strong encryption ciphers"
        )
    
    def disable_weak_macs(self) -> bool:
        """
        禁用弱MAC算法
        返回:
            bool: 是否设置成功
        """
        return self._update_config(
            "MACs", 
            "hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com",
            "Strong MAC algorithms"
        )
    
    def enable_ssh_v2(self) -> bool:
        """
        强制使用 SSH 协议版本 2
        返回:
            bool: 是否设置成功
        """
        return self._update_config("Protocol", "2", "SSH protocol version")
    
    def restrict_users(self, allow_users: Optional[List[str]] = None, deny_users: Optional[List[str]] = None) -> bool:
        """
        限制允许/拒绝的用户
        参数:
            allow_users: 允许的用户列表
            deny_users: 拒绝的用户列表
        返回:
            bool: 是否设置成功
        """
        success = True
        if allow_users:
            users = " ".join(allow_users)
            success &= self._update_config("AllowUsers", users, "Allowed SSH users")
        if deny_users:
            users = " ".join(deny_users)
            success &= self._update_config("DenyUsers", users, "Denied SSH users")
        
        return success
    
    def apply_changes(self) -> bool:
        """
        应用所有更改(重启SSH服务)
        返回:
            bool: 是否重启成功
        """
        # 检查配置语法
        success, output = self.run_command(["sshd", "-t"])
        if not success:
            print("Config test failed: {}".format(output))
            return False
        
        # 重启SSH服务
        success, output = self.run_command(["systemctl", "restart", "sshd"])
        if not success:
            print("Failed to restart SSH: {}".format(output))
            return False
        
        return True
    
    def read_config(self) -> Dict[str, str]:
        """
        获取当前SSH配置
        返回:
            Dict[str, str]: 配置键值对
        """
        return self.read_line_config2dict(self.sshd_config_path, ' ')

    def write_config(self, config: Dict, config_file=None) -> bool:
        """
        写入 SSH 配置文件
        参数:
            config: 配置字典
        返回:
            bool: 是否写入成功
        """
        config_file = config_file or self.sshd_config_path
        new_config = {}
        for key, value in config.items():
            if isinstance(value, bool):
                new_config[key] = 'yes' if value else 'no'
            else:
                new_config[key] = value
        return self.write_dict2line_config(config_file, new_config, ' ')
    
    
    def get_prefer_config(self):
        return self.config
    
    def backup(self, suffix=None):
        ssh_file = self.backup_file(self.ssh_config_path,suffix=suffix )
        sshd_file  = self.backup_file(self.sshd_config_path,suffix=suffix )

        bakcup_info = { 'ssh_file' : ssh_file,
                 'sshd_file': sshd_file
                }
        
        return { 'SSHManager' : bakcup_info }
        
    def restore(self, config):
        ssh_file = config.get('ssh_file')
        sshd_file = config.get('sshd_file')
        if ssh_file:
            self.restore_file(ssh_file, self.ssh_config_path)
        if sshd_file:
            self.restore_file(sshd_file, self.sshd_config_path)




class ServiceManager(SecBase):
    def __init__(self):
      super().__init__()
      self.system_config =  {
          'disable': [],
          'mask': []
       }

      self.config = {
          'disable': [ 'bluetooth', 'cups' , 'vsftpd', 'nmbd', 'smbd', 'samba-ad-dc', 'httpd' , 'rpcbind', 'nfs-server', 'apache2', 'bind9' ],
          'mask': []
       }
      
      self.ui_config ={
            'id': 'service',  
            'category': '服务配置',        
            'uis': OrderedDict([
                ( "disable", ("禁用服务", "table" ) ),
            ]),
            'disable_columns': OrderedDict([ ('unit','服务'), ('load', '加载'), ('active', '激活'), ('sub', '状态'), ('description','描述') ]),
            # 'disable_columns': OrderedDict([ ('unit','服务')]),
            'disable_combo_columns': {
                'unit': [ service['unit'] for service in self.get_all_services() ],               
            },
            'disable_data': [ self.get_service_status(s) for s in self.config.get('disable') ]
        }

        # 获取系统中所有启动的服务   
        #   self.get_all_services()  
      
    
    def get_prefer_config(self):
        return self.config
    

    def read_config(self):
        self.system_config.clear()
        self.system_config['disable']=[]
        self.system_config['mask']=[]
        
        success, output = self.run_command(['systemctl', 'list-unit-files', '--type=service', '--no-pager', '--no-legend'])
        if not success:
            return self.system_config
      
        for line in output.splitlines():
            parts = re.split(r'\s+', line.strip(), maxsplit=2)
            if len(parts) >= 3:
                unit, state, preset = parts
                if state == 'enabled':
                    self.system_config['disable'].append(unit.split('.service')[0])
                elif state == 'masked':
                    self.system_config['mask'].append(unit.split('.service')[0])
                else:
                    continue
        return self.system_config
      
    def write_config(self, config : Dict = None):
        if config is None : config = self.config.get('disable')
        for action,services in config.items():
            for service in services:
                self.do_action_service(service, action)
      
    def get_update_config(self):
        self.update_config = self.read_config()
  
    def do_all_service(self, actions:Dict):
        '''
        根据插入的字典 {'service_name', [ action1, action2 ] } 执行动作
        '''
        for service_name,action_list in actions.items():
            for action in action_list:
                self.do_action_service(service_name,action)
                
    def get_all_services(self) -> Dict[str, str]:
    
        """
        获取所有systemd服务
        
        参数:
            filter_active: 是否只返回.service单元(默认True)
            
        返回:
        { 'xendomains': {'unit': '.service', 'load': 'not-found', 'active': 'inactive', 'sub': 'dead', 'description': ''}}
          
        """
        
        self.service_list = []      
        success, output = self.run_command(['systemctl', 'list-units', '--type=service', '--state=active', '--no-pager', '--no-legend'])
        if not success:
            return self.service_list
      
        for line in output.splitlines():
            # 示例行: auditd.service    loaded active running Security Auditing Service
            parts = re.split(r'\s+', line.strip(), maxsplit=4)
            if len(parts) >= 5:
                unit, load, active, sub, description = parts
                self.service_list.append(
                    {
                        'unit': unit.split('.service')[0],
                        'load': load,
                        'active': active,
                        'sub': sub,
                        'description': description
                    }
                )
        
        return self.service_list
    
    
    def get_service_status(self,service_name:str):
        data = self.get_service_detail(service_name, ['Description','LoadState','SubState'] )
        propertys ={ 'unit': service_name }
        for key,value in data.items():
            if key == 'Description': 
                propertys['description']=value
            elif key == 'LoadState':
                propertys['load']=value
            elif key == 'ActiveState': 
                propertys['active']=value
            elif key == 'SubState':
                propertys['sub']=value
        return propertys

    def get_row_data(self, key):
        ''' 用于 UI 更新数据 '''
        return self.get_service_status(service_name=key)
    
    def get_service_detail(self, service_name: str, property_list: list=None) -> Dict[str, str]:
        """
        获取服务详细状态
        
        参数:
            service_name: 服务名称(带或不带.service后缀)
            
        返回:
            Dict[str, str]: 服务状态信息
        """
        if not service_name.endswith('.service'):
            service_name += '.service'
        
        if property_list:
            success, output = self.run_command(['systemctl', 'show', '--property={}'.format(','.join(property_list)), service_name])
        else:
            success, output = self.run_command(['systemctl', 'show', '--no-pager', service_name])
            
        if not success:
            return {'error': output}
        
        status = {}
        for line in output.splitlines():
            if '=' in line:
                key, value = line.split('=', 1)
                status[key] = value
        
        # 添加简化的状态信息
        success, active_state = self.run_command(['systemctl', 'is-active', service_name])
        status['ActiveState'] = active_state if success else 'unknown'
        
        # success, enabled_state = self.run_command(['systemctl', 'is-enabled', service_name])
        # status['UnitFileState'] = enabled_state if success else 'unknown'
        
        return status
    
    def do_action_service(self, service_name: str, command: str) -> Tuple[bool, str]:
        """
        启动服务
        
        参数:
            service_name: 服务名称(带或不带.service后缀)
            
        返回:
            Tuple[bool, str]: (是否成功, 输出信息)
        """
        if not service_name.endswith('.service'):
            service_name += '.service'
        if command == 'disable':
            cmd  = [ 'systemctl', command ,'--now', service_name]
        else:
            cmd  = [ 'systemctl', command ,'--now', service_name]
        result,output = self.run_command(cmd)
        self.logger.info('执行：{} => {}'.format(' '.join(cmd), 'sucess' if result else 'failed'))
        return result
        

    
    def get_service_logs(self, service_name: str, lines: int = 50) -> Tuple[bool, str]:
        """
        获取服务日志
        
        参数:
            service_name: 服务名称(带或不带.service后缀)
            lines: 要获取的日志行数(默认50)
            
        返回:
            Tuple[bool, str]: (是否成功, 日志内容)
        """
        if not service_name.endswith('.service'):
            service_name += '.service'
        return self.run_command(['journalctl', '-u', service_name, '-n', str(lines), '--no-pager'])
    

    def get_disabled_services(self):
        # 获取所有服务状态（systemd系统）
        cmd = "systemctl list-unit-files --type=service --no-legend"
        result, output = self.run_command(cmd)
        
        disabled_services = []
        if result:
            for line in output.split('\n'):
                if line.strip() and "disabled" in line.lower():
                    service = line.split()[0]
                    disabled_services.append(service)
        return disabled_services
        
    
    def backup(self, suffix=None):
        disable_service = self.get_disabled_services()
        backup_info = { 'disable_service' : disable_service }
        return { 'ServiceManager': backup_info }
        
    def restore(self, config):
        disable_service = config.get('disable_service')
        current_disable_service = self.get_disabled_services()
        for service in current_disable_service:
            if service not in disable_service:
                print("正在启动服务 {}".format(service))
                self.run_command(['systemctl', 'enable', '--now', service ])



'''
禁止指定 IP
iptables -I INPUT -s 10.0.28.15 -j DROP
iptables -I INPUT -s 10.0.28.15,10.0.28.1 -jDROP
iptables -I INPUT -m iprange --src-range 10.0.28.1-10.0.28.3 -j DROP
禁止指定 IP段
iptables -I INPUT -s 10.0.28.15/24 -j DROP
禁止指定 IP和端口
iptables -I INPUT -s 10.0.28.15 -ptcp --dport 22 -j DROP
iptables -A INPUT -p tcp -m multiport --dports 16501:16800 -j ACCEPT

iptables -A INPUT -p icmp --icmp-type 8 -j DROP
iptables -A INPUT -p tcp --dport 139 -j DROP
iptables -I INPUT -s 192.168.4.10 -p tcp --dport 22 -j DROP
iptables -A INPUT -p tcp --dport 22  -s 192.168.4.0/24  -j  DROP
iptables -A INPUT -p tcp -m multiport --dport 21:25,135:139 -j DROP
iptables -A INPUT -p tcp --dport 22 -m mac --mac-source 52:54:00:65:44:B1  -j  DROP
iptables -A INPUT -p tcp --dport 22 -m iprange --src-range 192.168.4.10-192.168.4.20   -j  ACCEPT
yum -y install iptables-services
systemctl start iptables.service
systemctl enable iptables.service
service  iptables save
ptables其他拓展模块还有很多，可以通过 man iptables-extensions 命令去查看。

参考：
    https://www.cnblogs.com/keystone/p/10763947.html

'''

class IPTablesManager(SecBase):
    """
    iptables 规则管理类，支持读取、添加、删除规则
    功能：
      - 读取现有规则并解析为结构化数据
      - 动态生成符合语法的 iptables 命令
      - 自动选择 iprange/multiport 等模块
    """
    
    def __init__(self, table: str = "filter"):
        super().__init__()
        self.service_name = 'sectool.service'
        self.script_file = '/usr/bin/sectool.sh'
        self.table = table  # 默认操作表 (filter/nat/mangle 等)
        self.rules = []     # 存储当前内存中的规则
        self.config = [{'__cmd': '-A INPUT -p tcp -m iprange --src-range 192.168.1.1-192.168.1.5 -m multiport --dports 80,443 -j ACCEPT',
                        'action': 'ACCEPT',
                        'chain': 'INPUT',
                        'dst_port': '80,443',
                        # 'extra_args': '-m iprange  -m multiport',
                        'protocol': 'tcp',
                        'source_ip': '192.168.1.1-192.168.1.5'},]
        
        self.ui_config = {
            'id': 'iptables',
            'category': '防火墙',
            'uis': OrderedDict([
                ('iptables_mode', ("防火墙模式（INPUT）", "combo", ["白名单(默认拒绝)", "黑名单(默认放行)"], "黑名单(默认放行)") ), 
                ("iptables_rules", ("添加 iptables 规则（注意顺序）", "table") )
            ]),
            'iptables_rules_columns': OrderedDict([ ('chain', '方向'), ('source_ip','源地址'), ('dest_ip', '目标地址'), ('protocol', '协议'), ('src_port', '源端口'), ('dst_port', '目标端口'), ('action', '动作')]),
            'iptables_rules_combo_columns': {
                'action':[ 'ACCEPT','DROP', 'REJECT' ],
                'chain':[ 'INPUT', 'OUTPUT'],
                'protocol':['tcp', 'udp', 'all'],                
            },
            'iptables_rules_tips': {'源地址':'支持单IP、多IP(, 分隔)、连续IP(- 分隔)、网段 ', '目标地址':'支持单、多IP(, 分隔)、连续IP(- 分隔)、网段',
                                   '源端口': '支持单端口、多端口(, 分隔)、连续端口(: 分隔)', '目标端口': '支持单端口、多端口(, 分隔)、连续端口(: 分隔)'},
            'iptables_rules_defaults' : ({'action': 'ACCEPT',
                                        'chain': 'INPUT',
                                        'src_port': '0:65535',
                                        'dst_port': '0:65535',
                                        'protocol': 'all',
                                        'source_ip': '0.0.0.0/0',
                                        'dest_ip': '0.0.0.0/0'}),
            'iptables_rules_data' : (
                    {
                        "chain": "INPUT",
                        "source_ip": "0.0.0.0/0",
                        "dest_ip": "0.0.0.0/0",
                        "protocol": "tcp",
                        "src_port": "0:65535",
                        "dst_port": "135,139,445",
                        "action": "DROP"
                    },
                    {
                        "chain": "INPUT",
                        "source_ip": "0.0.0.0/0",
                        "dest_ip": "0.0.0.0/0",
                        "protocol": "tcp",
                        "src_port": "0:65535",
                        "dst_port": "5800,539,593",
                        "action": "DROP"
                    },
                    {
                        "chain": "INPUT",
                        "source_ip": "0.0.0.0/0",
                        "dest_ip": "0.0.0.0/0",
                        "protocol": "udp",
                        "dst_port": "137,138,69,1434,539",
                        "action": "DROP"
                    }
                )
        }
        

    def _parse_matches(self, match_str: str) -> Dict:
        """解析匹配条件（如 -s/--src-range、--dport 等）"""
        parsed = {}
        # 匹配 IP 和端口规则
        ip_pattern = re.compile(r"-(s|d) ([\d\.\/\-]+)")
        iprange_pattern = re.compile(r"--(src|dst)-range ([\d\.\-]+)")
        port_pattern = re.compile(r"--(s|d)ports? ([\d\:,]+)")
        protocol_pattern = re.compile(r"-p (\w+)")

        # 处理标准 IP (-s/-d)
        for m in ip_pattern.finditer(match_str):
            key = "source_ip" if m.group(1) == "s" else "dest_ip"
            parsed[key] = m.group(2)
            match_str = match_str.replace(m.group(0), "")

        # 处理 iprange 模块
        for m in iprange_pattern.finditer(match_str):
            key = "source_ip" if m.group(1) == "src" else "dest_ip"
            parsed[key] = m.group(2)
            match_str = match_str.replace(m.group(0), "")  

        # 处理端口
        for m in port_pattern.finditer(match_str):
            key = "src_port" if m.group(1) == "s" else "dst_port"
            parsed[key] = m.group(2)
            match_str = match_str.replace(m.group(0), "")
        
        # 匹配协议
        protocol_match = protocol_pattern.search(match_str)
        if protocol_match:
            parsed["protocol"] = protocol_match.group(1)
            match_str = match_str.replace(protocol_match.group(0), "")
        
        # 未匹配的参数
        extra = match_str.replace('-m iprange','')
        extra = extra.replace('-m multiport','')
        parsed["extra_args"] = extra.strip()

        return parsed

    def _generate_rule_command( self, chain: str, action: str, 
                                source_ip: Optional[str] = None, dest_ip: Optional[str] = None,
                                src_port: Optional[str] = None, dst_port: Optional[str] = None, 
                                protocol: Optional[str] = None, extra_args: Optional[str] = None) -> str:
        """ 生成 iptables 命令 """
        parts = [
            "iptables", "-t", self.table, "-A", chain,
            *self._build_protocol(protocol),
            *self._build_ip_match("source", source_ip),
            *self._build_ip_match("dest", dest_ip),
            *self._build_port_match("sport", src_port),
            *self._build_port_match("dport", dst_port),
            extra_args if extra_args else [],
            "-j", action
        ]
        cmd = " ".join(filter(None, parts))
        self.logger.debug("iptables cmd = {}".format(cmd))
        return cmd
    
    def _build_ip_match(self, prefix: str, value: Optional[str]) -> List[str]:
        """构建 IP 匹配参数"""
        if not value:
            return []
        prefix = 'dst' if 'dest' in prefix else 'src'
        if "-" in value:
            return ["-m", "iprange", "--{}-range".format(prefix), value]
        else:
            return ["-{}".format(prefix[0]), value]

    def _build_port_match(self, port_type: str, port: Optional[str]) -> List[str]:
        """构建端口匹配参数"""
        if not port:
            return []
        if "," in port or ":" in port:
            return ["-m", "multiport", "--{}s".format(port_type), port]
        else:
            return ["--{}".format(port_type), port]

    def _build_protocol(self, protocol: Optional[str]) -> List[str]:
        """构建协议参数（如 -p tcp）"""
        if protocol:
            return ["-p", protocol]
        return []
    
    
    def read_rules(self) -> None:
        """从系统读取并解析当前 iptables 规则"""
        self.rules.clear()
        _, output = self.run_command(['iptables-save', '-t' ,self.table])
        
        # 解析规则的正则表达式
        rule_pattern = re.compile(
            r".*?-A (?P<chain>\w+)"
            r"(?P<matches>.*?)"
            r"-j (?P<action>ACCEPT|DROP|REJECT)"
        )
        
        for line in output.splitlines():
            if line.startswith("-A"):
                match = rule_pattern.search(line)
                if not match:
                    continue
                chain = match.group('chain')
                action = match.group('action')
                matches = match.group("matches").strip()
                
                # 解析匹配条件
                parsed = self._parse_matches(matches)
                self.rules.append({
                    "chain": chain,
                    **parsed,
                    "action": action,
                    "__cmd": line
                })
        return self.rules
    
    
    def add_rule( self,  rule: Dict):
        """添加新规则到系统和内存"""
        cmd = rule.get('__cmd')
        if not cmd :
            cmd = self._generate_rule_command(
                chain=rule.get('chain'),
                action=rule.get('action'),
                source_ip=rule.get('source_ip'),
                dest_ip=rule.get('dest_ip'),
                src_port=rule.get('src_port'),
                dst_port=rule.get('dst_port'),
                protocol=rule.get('protocol'),
                extra_args=rule.get('extra_args'),
            )
        self.logger.debug('iptables cmd = {}'.format(cmd))
        result,output = self.run_command(cmd)
        if result:
            with open(self.script_file, 'a') as f:
                f.write('{}\n'.format(cmd))


    def delete_rule(self, rule: Dict) -> None:
        """删除指定规则"""
        print("Deleting rule: {}".format(rule))
        cmd = self._generate_rule_command(
            chain=rule["chain"],
            action=rule["action"],
            source_ip=rule.get("source_ip"),
            dest_ip=rule.get("dest_ip"),
            src_port=rule.get("src_port"),
            dst_port=rule.get("dst_port"),
            protocol=rule.get("protocol"),
            extra_args=rule.get('extra_args')
        ) if rule.get("__cmd") is None else 'iptables ' + rule["__cmd"]
        print('delete cmd = ',cmd)
        cmd = cmd.replace("-A", "-D")  # 将添加命令改为删除
        self.run_command(cmd)

    def add_startup_service(self):
        template = '''
[Unit]
Description=SecTool Security Policy Service
After=network.target
Requires=network.target

[Service]
Type=simple
User=root
ExecStart={}
Restart=no

[Install]
WantedBy=multi-user.target
'''.format(self.script_file)
        # if os.path.exists(self.script_file):
        with open(self.script_file, 'w') as f:
            f.write('#!/bin/bash\n')
        self.run_command('chmod +x {}'.format(self.script_file))
        with open('/etc/systemd/system/{}'.format(self.service_name), 'w') as f:
            f.write(template)
        self.run_command('systemctl daemon-reload')
        self.run_command('systemctl enable {}'.format('sectool'))

    
    def read_config(self, chain: Optional[str] = None) -> List[Dict]:
        """列出当前内存中的规则"""
        self.read_rules()
        if chain:
            self.rules = [r for r in self.rules if r["chain"] == chain]
        return {'iptables_rules': self.rules }
    
    
    def write_config(self, rules:dict):
        '''
        {'iptables_rules': [{'chain': 'INPUT', 'source_ip': '192.168.1.1-192.168.1.5', 'protocol': 'tcp', 'dst_port': '80,443', 'action': 'ACCEPT'}]}
        '''
        # 首先添加开机启动
        self.add_startup_service()
        
        # 防火墙模式
        mode = rules.get('iptables_mode')
        if '黑名单' in mode:
            cmd = 'iptables -P INPUT ACCEPT'
        else:
            cmd = 'iptables -P INPUT DROP'
        result,output = self.run_command(cmd)
        if result:
            with open(self.script_file, 'a') as f:
                f.write('{}\n'.format(cmd))
        
        
        rules = rules.get('iptables_rules')
        if rules: 
            for rule in rules:
                self.add_rule(rule)
                
        return True


    def backup(self, suffix=None):
        script_file = ''
        service_file = ''
        service_file_src = '/etc/systemd/system/{}'.format(self.service_name)
        if os.path.exists(self.script_file):
            script_file = self.backup_file(self.script_file,suffix=suffix )
        if os.path.exists(service_file_src):
            service_file  = self.backup_file(service_file_src,suffix=suffix )

        bacup_info = { 'script_file' : script_file,
                 'service_file': service_file
                }
        return { 'IPTablesManager': bacup_info }
        
    def restore(self, config):
        script_file = config.get('script_file')
        service_file = config.get('service_file')
        if script_file:
            # 删除添加的规则
            with open(self.script_file) as current, open(script_file) as old:
                old_rules = old.readlines()
                for line in current:
                    if not line : continue
                    if line not in old_rules:
                        line = line.strip()
                        self.logger.info('规则 {} 需要还原'.format(line))
                        if re.match(r'\s*iptables.*-P\s+INPUT(.*)', line):
                            print('-------')
                            action = 'ACCEPT'
                            for old in  old_rules:
                                result = re.match(r'\s*iptables.*-P\s+INPUT(.*)', old)
                                if result : action = result.group(1)
                            cmd = 'iptables -P INPUT {}'.format(action)
                        else:
                            cmd = re.sub(r"-(A|I)", "-D", line)
                        if cmd:
                            print(cmd)
                            self.run_command(cmd)
                    
            self.restore_file(script_file, self.script_file)
        elif os.path.exists(self.script_file):
            os.remove(self.script_file)
            
        service_file_src = '/etc/systemd/system/{}'.format(self.service_name)
        if service_file:
            self.restore_file(service_file, service_file_src)
        elif os.path.exists(service_file_src):
            os.remove(service_file_src)




class AuditLogManager(SecBase):
    def __init__(self):
        """初始化审计日志管理器"""
        super().__init__()
        self.grub_cfg = "/etc/default/grub"
        self.logrotate_file = '/etc/logrotate.d/auditd'
        self.RULES_FILE = "/etc/audit/rules.d/my-audit.rules"

        self.config = {
            'enable' :  True,
            'savelog_time' :  180,
            'path_rules' : [{'path': '/etc', 'perms': 'rwx', 'key': 'etc_access'}
                      ],
            'syscall_rules': [{'syscall': 'execve', 'key':  'execve'}   # ausyscall --dump 查看
                            ]
        }
        self.ui_config ={
            'id': 'audit',
            'category': '审计日志',
            'uis': OrderedDict([
                 ( "enable", ("是否开启审计日志（需要重启系统）", "check", False) ),
                 ( 'savelog_time' , ( "审计日志保留时间（月）", 'entry', 6 ) ),
                 ( "path_rules", ("添加监控路径", "table") ),
                 ( "syscall_rules" , ("添加系统调用", "table") ),
            ]),
            'path_rules_columns': OrderedDict([ ('path', '路径'), ('perms', '权限'), ('key', '标识') ]),
            'path_rules_combo_columns': {
                'perms':[ 'rwa','rwxa', 'r', 'w', 'x', 'a' ],
            },
            'syscall_rules_columns': OrderedDict([ ('syscall', '系统调用'), ('key', '标识') ]),
            'syscall_rules_combo_columns': {
                'syscall': self.get_all_syscall(),
            },
        }


    def get_all_syscall(self):
        result, output = self.run_command('ausyscall --dump')
        self.syscalls = []
        if result:
            for line in output.split('\n'):
                match = re.match(r'\d+\s*(\w*)',line)
                if match:
                    syscall = match.group(1).strip()
                    if syscall:
                        self.syscalls.append( syscall)
        return self.syscalls
                
    def enable_audit_at_boot(self) -> Tuple[bool, str]:
        """
        启用启动时审计功能(添加audit=1到GRUB配置)
        
        返回:
            Tuple[bool, str]: (是否成功, 输出信息)
        """
        if self.update_grub_cmdline('audit', '1'):
            self.run_command('systemctl enable auditd')
            self.run_command('systemctl start auditd')
            return True, "Audit enabled at boot. Reboot to take effect."
        return False, "Failed to enable audit at boot"
    
    def disable_audit_at_boot(self) -> Tuple[bool, str]:
        """
        禁用启动时审计功能(从GRUB配置移除audit=1)
        
        返回:
            Tuple[bool, str]: (是否成功, 输出信息)
        """
        if self.update_grub_cmdline('audit', remove=True):
            self.run_command('systemctl stop auditd')
            self.run_command('systemctl disable auditd')
            return True, "Audit disabled at boot. Reboot to take effect."
        return False, "Failed to disable audit at boot"
    
    def is_audit_enabled_at_boot(self) -> bool:
        """
        检查是否启用了启动时审计
        
        返回:
            bool: 是否启用
        """
        try:
            with open(self.grub_cfg, 'r') as f:
                for line in f:
                    if line.startswith('GRUB_CMDLINE_LINUX_DEFAULT='):
                        match = re.search(r'audit(?:=1)?', line)
                        return match is not None
        except Exception:
            return False
        return False
    
    # 以下是之前的所有其他方法...
    # service_status(), start_service(), list_rules() 等保持不变
    def add_syscall_rule(self, syscall, key=None, arch='b64', action='exit,always'):
        """
        添加系统调用审计规则
        :param syscall: 要监控的系统调用名称（如open、execve）
        :param key: 规则标识关键字
        :param arch: 系统架构（b32/b64）
        :param action: 规则触发动作（exit,always / entry,always）
        """
        try:
            if not syscall : return False
            if key is None: key = 'sec_{}'.format(syscall)
            cmd = [
                'sudo', 'auditctl',
                '-a', action,
                '-F', 'arch={}'.format(arch),
                '-S', syscall,
                '-k', key
            ]
            subprocess.run(cmd, check=True, stderr=subprocess.PIPE)
            self.logger.info("成功添加系统调用规则: {} ({})".format(syscall, key))
        except subprocess.CalledProcessError as e:
            self.logger.info("添加系统调用规则失败: {}".format(e.stderr.decode().strip()))

    def add_path_rule(self, path, perms, key=None):
        """
        添加路径审计规则
        :param path: 要监控的文件/目录路径
        :param perms: 监控的操作权限（r=读, w=写, x=执行, a=属性变更）
        :param key: 规则标识关键字
        """
        try:
            if not path : return False
            if key is None: 
                base_name = 'root' if path == '/' else os.path.basename(path.rstrip('/'))
                key = 'sec_{}'.format(base_name)
            cmd = [
                'sudo', 'auditctl',
                '-w', path,
                '-p', perms,
                '-k', key
            ]
            subprocess.run(cmd, check=True, stderr=subprocess.PIPE)
            self.logger.info("成功添加路径规则: {} ({})".format(path, key))
        except subprocess.CalledProcessError as e:
            self.logger.info("添加路径规则失败: {}".format(e.stderr.decode().strip()))


    def delete_rule(self, key):
        """
        根据关键字删除审计规则
        :param key: 要删除规则的标识关键字
        """
        try:
            cmd = ['sudo', 'auditctl', '-D', '-k', key]
            subprocess.run(cmd, check=True, stderr=subprocess.PIPE)
            self.logger.info("成功删除规则: {key}".format(key))
        except subprocess.CalledProcessError as e:
            self.logger.info("删除规则失败: {}".format(e.stderr.decode().strip()))

    def list_rules(self):
        """列出当前所有审计规则"""
        try:
            cmd = ['sudo', 'auditctl', '-l']
            result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE)
            self.logger.info("当前审计规则：")
            self.logger.info(result.stdout.decode())
            return result.stdout.decode()
        except subprocess.CalledProcessError as e:
            self.logger.info("获取规则列表失败: {}".format(e.stderr.decode().strip()))
            return ""
    
    def _reload_rules(self):
        """重新加载审计规则"""
        try:
            subprocess.run(
                ['sudo', 'auditctl', '-R', self.RULES_FILE],
                check=True,
                stderr=subprocess.PIPE
            )
            self.logger.info("审计规则重载成功")
        except subprocess.CalledProcessError as e:
            self.logger.info("规则重载失败: {}".format(e.stderr.decode().strip()))

    def _write_rule(self, rule_line):
        """将规则写入配置文件"""
        try:
            # 检查规则是否已存在
            existing_rules = []
            if os.path.exists(self.RULES_FILE):
                with open(self.RULES_FILE, 'r') as f:
                    existing_rules = f.readlines()

            # 防止重复添加
            if rule_line + '\n' not in existing_rules:
                with open(self.RULES_FILE, 'a') as f:
                    f.write(rule_line + '\n')
                self.logger.info("永久规则已写入: {}".format(rule_line))
                return True
            else:
                self.logger.info("跳过规则已存在：{}".format(rule_line))
                return False
        except PermissionError:
            self.logger.info("需要root权限操作配置文件")
            return False
        except Exception as e:
            self.logger.info("写入配置文件失败:{} {}".format(str(e),rule_line))
            return False


    def add_permanent_syscall_rule(self, syscall, key=None, arch='b64', action='exit,always'):
        """添加永久系统调用规则"""
        if key is None: key = 'sec_{}'.format(syscall)
        rule_line = "-a {action} -F arch={arch} -S {syscall} -k {key}".format(syscall=syscall, key=key, arch=arch, action=action)
        if self._write_rule(rule_line):
            self._reload_rules()

    def add_permanent_path_rule(self , path, perms, key=None):
        """添加永久路径监控规则"""
        if key is None: 
                base_name = 'root' if path == '/' else os.path.basename(path.rstrip('/'))
                key = 'sec_{}'.format(base_name)
        rule_line = "-w {path} -p {perms} -k {key}".format(path=path, perms=perms, key=key)
        if self._write_rule(rule_line):
            self._reload_rules()


    def remove_permanent_rule(self, key):
        """删除永久规则"""
        try:
            # 读取现有规则
            if not os.path.exists(self.RULES_FILE):
                self.logger.info("规则文件不存在")
                return

            with open(self.RULES_FILE, 'r') as f:
                lines = f.readlines()

            # 过滤包含指定key的规则
            new_lines = [line for line in lines if "-k {}".format(key) not in line]

            # 检查是否有变化
            if len(new_lines) == len(lines):
                self.logger.info("未找到使用key: {}的规则".format(key))
                return

            # 写回文件
            with open(self.RULES_FILE, 'w') as f:
                f.writelines(new_lines)
            
            self.logger.info("已删除所有使用key: {}的规则".format(key))
            self._reload_rules()
        except PermissionError:
            self.logger.info("错误：需要root权限操作配置文件")
        except Exception as e:
            self.logger.info("删除规则失败: {}".format(str(e)))

    def list_permanent_rules(self):
        """列出配置文件中的规则"""
        try:
            if os.path.exists(self.RULES_FILE):
                with open(self.RULES_FILE, 'r') as f:
                    rules = f.read()
                self.logger.info("永久审计规则：")
                self.logger.info(rules)
                return rules
            else:
                print("自定义规则文件不存在")
        except Exception as e:
            self.logger.info("读取规则文件失败: {str(e)}".format(str(e)))
            print(e)

    def change_savelog_time(self, savelog_time):
        if savelog_time == 0 :
           if os.path.exists(self.logrotate_file):
                os.remove(self.logrotate_file)
                return True
        logrotate_rule = '''/var/log/audit/*.log {
    rotate %s
    monthly
    missingok
    notifempty
    maxsize 100M
}
'''%(savelog_time)
        with open(self.logrotate_file, 'w') as f:
            f.write(logrotate_rule)
        return True
            
        
    def get_prefer_config(self):
        return self.config
    
    def read_config(self):
        # 获取日志保存时间
        if os.path.exists(self.logrotate_file):
            with open(self.logrotate_file , 'r') as f:
                for line in f:
                    result = re.match(r'.*rotate\s+(\d+).*',line )
                    if result:
                        savelog_time = result.group(1)
        else:
            savelog_time = 0
            
        # 获取审计规则
        rule_str = self.list_permanent_rules()
        path_rules = []
        syscall_rules = []
        if rule_str:
            path_pattern = re.compile(
                                    r'^-w\s+(\S+)\s+'          # 监控路径
                                    r'-p\s+([rwax]+)\s*'       # 权限
                                    r'(?:-k\s+(\S+))?'         # 可选关键字
                                    r'\s*$'                    # 行尾
                                )
            syscall_pattern = re.compile(
                            r'^-a\s+(\w+,\w+)\s+'      # 动作
                            r'-F\s+arch=(\w+)\s+'      # 架构
                            r'-S\s+(\S+)\s*'           # 系统调用
                            r'(?:-k\s+(\S+))?'         # 可选关键字
                            r'\s*$'                    # 行尾
                        )
            
            for rule in rule_str.split('\n'):
                match = path_pattern.match(rule)
                if match:
                    result = match.groups()
                    key = result[2] if len(result) >2 else ''
                    path_rules.append( {
                        "path": result[0],
                        "perms": result[1],
                        "key": key
                        } )
                else:
                    match = syscall_pattern.match(rule)
                    if  match:
                        result = match.groups()
                        key = result[3] if len(result) >3 else ''
                        syscall_rules.append( {
                            "path": result[2],
                            "key": key
                            } )
        config = {
            "enable": self.is_audit_enabled_at_boot(),
            "savelog_time": savelog_time,
            "path_rules": path_rules,
            "syscall_rules": syscall_rules
        }
        return {'audit': config}
                        

    def write_config(self, config: dict=None):
        if config is None:
            config = self.config
        enable = config.get('enable')
        if enable:
            self.enable_audit_at_boot()
            
            savelog_time = config.get('savelog_time', 6)
            self.change_savelog_time(savelog_time)
            
            if not os.path.exists('/etc/audit/rules.d'):
                os.makedirs('/etc/audit/rules.d')
            # 清空自定义 my-audit.rules
            with open(self.RULES_FILE, 'w') as f:
                f.truncate()
            
            path_rules = config.get('path_rules',[])
            for  path_rule in path_rules:
                self.add_path_rule(**path_rule)
                self.add_permanent_path_rule(**path_rule)
            
            syscall_rules = config.get('syscall_rules',[])
            for  syscall_rule in syscall_rules:
                self.add_syscall_rule(**syscall_rule)
                self.add_permanent_syscall_rule(**syscall_rule)
                
            self.run_command('augenrules --load')
        else:
            self.disable_audit_at_boot()

    
    def backup(self, suffix=None):
        config = self.read_config()
        return { 'AuditLogManager': config.get('audit') }
        
    def restore(self, config):
        if config :
            self.write_config(config)
            self.run_command('augenrules --load')
            # self.run_command('systemctl restart auditd')


class Other(SecBase):
    
    def __init__(self):
        super().__init__()
        self.grub = GrubPasswordManager()
        self.shscirpt = ShScript()
        
        self.ui_config = {
            'id': 'other',
            'category': '其他配置',
            'uis': OrderedDict([
                *self.grub.ui_config['uis'].items(),
                *self.shscirpt.ui_config['uis'].items()
            ])
        }
        
    def write_config(self, value = None):
        grub_keys = self.grub.config.keys()
        shscrpt_keys = self.shscirpt.config.keys()
        grub_data = {}
        sh_data = {}
        for key,value in value.items():
            if key in grub_keys:
                grub_data[key] = value
            elif key in shscrpt_keys:
                sh_data[key] = value
        self.grub.write_config(grub_data)
        self.shscirpt.write_config(sh_data)
    
    def read_config(self):
        return {
            **self.grub.read_config(),
            **self.shscirpt.read_config()
        }

    def backup(self, suffix=None):
        grub_cfg_file = self.backup_file(self.grub.grub_cfg,suffix=suffix )
        grub_user_cfg_file  = self.backup_file(self.grub.grub_user_cfg,suffix=suffix )
        self.grub_backup = grub_cfg_file
        self.grub_user_backup = grub_user_cfg_file

        backup_info = { 'grub_cfg_file' : grub_cfg_file,
                 'grub_user_cfg_file': grub_user_cfg_file
                }
        return { 'Other' : backup_info }
        
    def restore(self, config):
        grub_cfg_file = config.get('grub_cfg_file')
        grub_user_cfg_file = config.get('grub_user_cfg_file')
        if grub_cfg_file:
            self.restore_file(grub_cfg_file, self.grub.grub_cfg)
        if grub_user_cfg_file:
            self.restore_file(grub_user_cfg_file, self.grub.grub_user_cfg)



class GrubPasswordManager(SecBase):
    def __init__(self):
        """初始化GRUB密码管理器"""
        super().__init__()
        self.grub_cfg = "/etc/default/grub"
        self.grub_user_cfg = "/etc/grub.d/40_custom"
        self.grub_backup = '/tmp/grub'
        self.grub_user_backup = '/tmp/grub_user'
        
        self.config = { 'enable_password' : False,
                        'username' : 'root',
                        'password': 'Kylin123123' ,
                       }
        self.ui_config = {
            'id': 'grub',
            'category': '其他配置',
            'uis': OrderedDict([
                ( "enable_password", ("GRUB 菜单加密", "check", False) ),
                ( "username", ("用户", "entry", "root") ),
                ( "password", ("密码", "entry", "Kylin123123") ),
            ])
        }
        
    def read_config(self):
        return self.config
    
    def write_config(self, value: dict = None):
        self.logger.info(value)
        if value is None: value = self.config
        enable = value.get('enable_password',False)
        if enable:
             username = value.get('username','root')
             password = value.get('password', 'Kylin123123')
             self.enable_password_protection(username, password)
        else:
            self.disable_password_protection()
                
    
    def _generate_password_hash(self, password: str) -> str:
        """
        生成GRUB可识别的PBKDF2密码哈希
        
        参数:
            password: 明文密码
            
        返回:
            str: GRUB格式的密码哈希
        """
        try:
            # 使用grub-mkpasswd-pbkdf2生成哈希
            cmd = shutil.which('grub-mkpasswd-pbkdf2')
            if cmd is None:
                cmd = shutil.which('grub2-mkpasswd-pbkdf2')
                if cmd is None:
                    raise FileNotFoundError('grub-mkpasswd-pbkdf2 not found')
            
            proc = subprocess.Popen(
                [ cmd ],
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                # text=True
                universal_newlines=True
            )
            stdout, stderr = proc.communicate(input="{}\n{}\n".format(password,password))            
            if proc.returncode == 0:
                # 提取哈希部分
                match = re.match(r'.*(grub\.pbkdf2\.sha512\.\d+\.[.A-Fa-f0-9\.]+)', stdout, re.MULTILINE|re.DOTALL)
                if match:
                    return match.group(1)
            else:
                # 如果命令不可用，使用Python实现简单版本(不建议生产环境使用)
                print("Warning: Using fallback password hashing (less secure)")
                salt = os.urandom(16).hex()
                iterations = 10000
                dk = hashlib.pbkdf2_hmac(
                    'sha512',
                    password.encode('utf-8'),
                    salt.encode('utf-8'),
                    iterations
                )
                return "grub.pbkdf2.sha512.{}.{}.{}".format(iterations, salt, dk.hex())
            
        except Exception as e:
            print("Password generation error: {}".format(str(e)))
            raise RuntimeError("Failed to generate password hash") from e
    
    def add_unrestricted(self) -> bool:
        linux_file = '/etc/grub.d/10_linux'
        temp='/tmp/10_linue'
        is_found = False
        with open(linux_file, 'r') as src, open(temp,'w+') as dest:
            dest.truncate()
            for line in src:
                result=re.match(r'^\s*CLASS="(.*)"',line)
                if is_found == False and result :
                    if '--unrestricted' not in line:
                        line = 'CLASS="{} --unrestricted"\n'.format(result.group(1))
                    is_found = True
                    self.logger.info(line)
                dest.write(line)
        self.backup_file(linux_file)
        self.restore_file(temp,linux_file)
        os.chmod(linux_file,0o755)

    def enable_password_protection(self, username: str, password: str, superusers: Optional[List[str]] = None) -> Tuple[bool, str]:
        """
        启用GRUB密码保护
        
        参数:
            username: GRUB用户名
            password: GRUB密码
            superusers: 超级用户列表(无需密码的用户)
            
        返回:
            Tuple[bool, str]: (是否成功, 输出信息)
        """
        # 备份原始文件
        self.grub_backup=self.backup_file(self.grub_cfg, self.grub_backup)
        self.grub_user_backup=self.backup_file(self.grub_user_cfg, self.grub_user_backup)
        
        try:
            # 1. 生成密码哈希
            password_hash = self._generate_password_hash(password)
            self.logger.info('{} 密码已加密: {}'.format(password,password_hash))
            
            # 2. 更新/etc/default/grub
            # with open(self.grub_cfg, 'r') as f:
            #     lines = f.readlines()
            
            # updated = False
            # new_lines = []
            # for line in lines:
            #     if line.startswith('GRUB_CMDLINE_LINUX='):
            #         # 确保不重复添加
            #         if 'GRUB_PASSWORD' not in line:
            #             new_line = line.strip()
            #             if new_line.endswith('"'):
            #                 new_line = new_line[:-1] + ' GRUB_PASSWORD=' + password_hash + '"'
            #             else:
            #                 new_line += ' GRUB_PASSWORD=' + password_hash
            #             new_lines.append(new_line + '\n')
            #             updated = True
            #         else:
            #             new_lines.append(line)
            #     else:
            #         new_lines.append(line)
            
            # if updated:
            #     with open(self.grub_cfg, 'w') as f:
            #         f.writelines(new_lines)
            
            self.disable_password_protection()
            # 3. 更新/etc/grub.d/40_custom
            with open(self.grub_user_cfg, 'a') as f:
                f.write("\n# GRUB Password Protection\n")
                f.write('set superusers="{}"\n'.format(username))
                if superusers:
                    f.write('password_pbkdf2 {} {}\n'.format(username,password_hash))
                    for user in superusers:
                        f.write('password_pbkdf2 {} --unrestricted\n'.format(user))
                else:
                    f.write('password_pbkdf2 {} {}\n'.format(username,password_hash))
            # 添加--unrestricted ,修复桌面自启动要求密码
            self.add_unrestricted()
            # 4. 更新GRUB配置
            success = self.update_grub_file()
            if not success:
                raise RuntimeError("Failed to update GRUB")
            
            return True, "GRUB password protection enabled. Reboot to take effect."
        
        except Exception as e:
            # 恢复备份
            self.restore_file(self.grub_backup,self.grub_cfg)
            self.restore_file(self.grub_user_backup,self.grub_user_cfg)
            return False, "Failed to enable GRUB password: {}".format(str(e))
    
    def disable_password_protection(self) -> Tuple[bool, str]:
        """
        禁用GRUB密码保护
        
        返回:
            Tuple[bool, str]: (是否成功, 输出信息)
        """
        # 备份原始文件
        # self.backup_file(self.grub_cfg)
        # self.backup_file(self.grub_user_cfg)
        
        try:
            # 1. 从/etc/default/grub中移除GRUB_PASSWORD
            # with open(self.grub_cfg, 'r') as f:
            #     lines = f.readlines()
            
            # updated = False
            # new_lines = []
            # for line in lines:
            #     if line.startswith('GRUB_CMDLINE_LINUX='):
            #         # 移除GRUB_PASSWORD参数
            #         new_line = re.sub(r'GRUB_PASSWORD=[^\s"]+', '', line)
            #         new_lines.append(new_line)
            #         updated = True
            #     else:
            #         new_lines.append(line)
            
            # if updated:
            #     with open(self.grub_cfg, 'w') as f:
            #         f.writelines(new_lines)
            
            # 2. 清空/etc/grub.d/40_custom中的密码设置
            if os.path.exists(self.grub_user_cfg):
                with open(self.grub_user_cfg, 'r') as f:
                    lines = f.readlines()
                
                new_lines = []
                in_password_section = False
                for line in lines:
                    if line.strip().startswith('# GRUB Password Protection'):
                        in_password_section = True
                        continue
                    if in_password_section and (line.strip().startswith('set superusers=') or 
                                             line.strip().startswith('password_pbkdf2 ')):
                        continue
                    if in_password_section and not line.strip():
                        continue
                    new_lines.append(line)
                
                with open(self.grub_user_cfg, 'w') as f:
                    f.writelines(new_lines)
            
            # 3. 更新GRUB配置
            success = self.update_grub_file()
            if not success:
                raise RuntimeError("Failed to update GRUB")
            
            return True, "GRUB password protection disabled. Reboot to take effect."
        
        except Exception as e:
            # 恢复备份
            self.restore_file(self.grub_backup, self.grub_cfg)
            self.restore_file(self.grub_user_backup, self.grub_user_cfg)
            return False, "Failed to disable GRUB password: {}".format(str(e))
    
    def is_password_protection_enabled(self) -> bool:
        """
        检查是否启用了GRUB密码保护
        
        返回:
            bool: 是否启用
        """
        try:
            # 检查/etc/default/grub
            with open(self.grub_cfg, 'r') as f:
                for line in f:
                    if 'GRUB_PASSWORD=' in line:
                        return True
            
            # 检查/etc/grub.d/40_custom
            if os.path.exists(self.grub_user_cfg):
                with open(self.grub_user_cfg, 'r') as f:
                    content = f.read()
                    if 'set superusers=' in content and 'password ' in content:
                        return True
            
            return False
        except Exception:
            return False
        

class ShScript(SecBase):
    
    def __init__(self):
        super().__init__()
        self.config = { "script_text": ''}
        self.ui_config = {
            'id': 'shscript',
            'category': '其他配置',
            'uis': OrderedDict([
                ( "script_text", ("额外执行的脚本", "scripteditor", '#!/bin/bash\n') ),
            ])
        }
        
    def write_config(self, value = None):
        self.logger.info(value)
        self.run(value.get('script_text'))
        
    def read_config(self):
        return self.config
        
    def run(self, script_content) -> bool:
        # 通过 bash 执行脚本内容   
        # result, _ = self.run_command( ["bash", "-c", script_content])
        # return result
        
        shell_bang = '#!/bin/bash'
        for line in script_content.split('\n'):
            line = line.strip()
            if line.startswith('#!'): 
                shell_bang = line
                break
        shell_bin = shell_bang.split('!')[1]
        
        # Create a temporary file to run
        with tempfile.NamedTemporaryFile(suffix='.tmp', delete=False, mode='w') as tmpfile:
            tmpfile.write(script_content)
            tmpfile_path = tmpfile.name
        
        try:
            # Run the script and capture output
            if 'sh' in shell_bin:
                cmd = [ shell_bin, tmpfile_path]
            elif 'python' in shell_bin:
            # Use the system Python to check syntax
                cmd = [ shell_bin, tmpfile_path]
            else:
                raise Exception('不支持的脚本类型：{}'.format(shell_bin))
            
            result = subprocess.run(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                # text=True
                universal_newlines=True
            )
            
            output = result.stdout if result.stdout else ""
            errors = result.stderr if result.stderr else ""
            
            if result.returncode == 0:
                self.logger.info("执行成功: {}".format(output))
            else:
                self.logger.error("执行输出 code: {}\n\n{}\n{}".format(result.returncode,output,errors))
                raise Exception("执行输出 code: {}\n\n{}\n{}".format(result.returncode,output,errors))
                
        except Exception as e:
            self.logger.error("执行出错: {}".format(str(e)))
            raise
        finally:
            os.unlink(tmpfile_path)
            
            
    def run_script(self, script_file:str) -> bool:
        # 读取脚本内容
        with open(script_file, "r") as f:
            script_content = f.read()
        if script_content:
            return self.run(script_content)
        return False

if os.environ.get('DISPLAY'):

    @Singleton
    class DoTaskWidget(Gtk.Frame):
        
        def __init__(self, parent):
            super().__init__()
            self.logger = logging.getLogger(__name__)
            # 全局异常处理，线程需要单独处理
            sys.excepthook=self.handle_exception
            self.ui_config = { 'id' : '__do_task', 
                            'category': '执行配置', 
                            'uis': {  "dotask":('执行任务', 'dotask') }
                            }
            # 初始化变量
            self.parent = parent
            self.is_running = False

            self.set_size_request(400, 500)
            self.set_border_width(10)
            
            # 创建主布局
            main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
            self.add(main_box)
            # 创建按钮栏
            self.create_button_box(main_box)
            # 创建状态栏
            self.create_status_bar(main_box)
            # 创建输出区域
            self.out_dialog = OutputLogBox(self)
            main_box.pack_start(self.out_dialog, True, True, 0)
            # 错误对话框
            self.err_dialog = ErrorDialog(self)

            # 连接信号
            self.connect("destroy", Gtk.main_quit)
            # 显示所有元素
            self.show_all()
        
        def create_button_box(self, parent):
            """创建按钮工具栏"""
            button_box = Gtk.ButtonBox(spacing=10)
            button_box.set_layout(Gtk.ButtonBoxStyle.CENTER)
            button_box.set_margin_bottom(10)
            parent.pack_start(button_box, False, False, 0)
            
            # 导出配置按钮
            export_config_btn = Gtk.Button.new_with_label("导出配置")
            export_config_btn.set_tooltip_text("将当前配置导出为文件")
            export_config_btn.connect("clicked", self.on_export_config)
            button_box.add(export_config_btn)
            
            export_config_btn = Gtk.Button.new_with_label("导入配置")
            export_config_btn.set_tooltip_text("导入之前保存的配置")
            export_config_btn.connect("clicked", self.on_import_config)
            button_box.add(export_config_btn)
            
            # 执行按钮
            self.execute_btn = Gtk.Button.new_with_label("执行操作")
            self.execute_btn.set_tooltip_text("执行配置操作")
            self.execute_btn.connect("clicked", self.on_execute_btn)
            button_box.add(self.execute_btn)
            
            # 导出日志按钮
            export_log_btn = Gtk.Button.new_with_label("导出日志")
            export_log_btn.set_tooltip_text("将执行日志导出为文件")
            export_log_btn.connect("clicked", self.on_export_log)
            button_box.add(export_log_btn)
            
            # 还原备份
            restore_btn = Gtk.Button.new_with_label("还原备份")
            restore_btn.set_tooltip_text("还原系统到某一次加固之前的状态")
            restore_btn.connect("clicked", self.on_restore_btn)
            button_box.add(restore_btn)
            self.restore_btn = restore_btn
        
        def create_status_bar(self, parent):
            """创建状态栏"""
            self.status_bar = Gtk.Statusbar()
            self.status_bar.set_margin_top(5)
            self.status_bar_context_id = self.status_bar.get_context_id("status")
            self.update_status("就绪")
            parent.pack_start(self.status_bar, False, False, 0)
            # 初始化弹出窗口
            self.popover = None
        
        
        def update_status(self, message):
            """更新状态栏"""
            self.status_bar.push(self.status_bar_context_id, message)
        
        
        def on_export_config(self, widget):
            """导出配置按钮点击事件"""
            self.out_dialog.add_output_line("开始导出配置...", "info")
            # # 创建文件选择对话框
            dialog = Gtk.FileChooserDialog(
                title="保存配置",
                parent=self.get_toplevel(),
                action=Gtk.FileChooserAction.SAVE,
                buttons=(
                    "取消", Gtk.ResponseType.CANCEL,
                    "保存", Gtk.ResponseType.OK
                )
            )

            # 设置默认文件名
            current_time = datetime.now().strftime("%Y%m%d_%H%M%S")
            dialog.set_current_name("config_export_{}.cfg".format(current_time))
            
            # 添加文件过滤器
            filter_cfg = Gtk.FileFilter()
            filter_cfg.set_name("配置文件 (*.cfg)")
            filter_cfg.add_pattern("*.cfg")
            dialog.add_filter(filter_cfg)
            
            filter_all = Gtk.FileFilter()
            filter_all.set_name("所有文件")
            filter_all.add_pattern("*")
            dialog.add_filter(filter_all)
            
            # 显示对话框
            response = dialog.run()
            
            if response == Gtk.ResponseType.OK:
                filename = dialog.get_filename()
                if not filename.endswith(".cfg"):
                    filename += ".cfg"
                    
                # 导出配置
                self.all_config = self.parent.dump_config()
                with open(filename,'w+') as f:
                    json.dump(self.all_config,f,indent=4)
                os.chmod(filename,0o777)
                self.out_dialog.add_output_line("导出配置到: {}".format(filename), "info")
                self.out_dialog.add_output_line("导入配置成功", "success")
                self.update_status("导入配置成功")
    
            else:
                self.out_dialog.add_output_line("配置导出已取消", "warning")
            
            dialog.destroy()
            
        
        def on_execute_btn(self, widget):
            """执行按钮点击事件"""
            if self.is_running:
                self.out_dialog.add_output_line("操作已在执行中，请等待完成", "warning")
                return
                
            self.is_running = True
            self.execute_btn.set_sensitive(False)
            self.out_dialog.add_output_line("开始执行操作...", "title")
            
            # 创建进度条
            self.progress_bar = Gtk.ProgressBar()
            self.progress_bar.set_show_text(True)
            self.progress_bar.set_fraction(0.0)
            self.status_bar.add(self.progress_bar)
            self.status_bar.show_all()
            
            # 执行操作
            # self.do_execute()
            Thread(target=self.do_execute,args=(self.handle_exception,)).start()
            # GLib.timeout_add(1000,self.do_execute)
        
        def on_export_log(self, widget):
            """导出日志按钮点击事件"""
            self.out_dialog.add_output_line("开始导出日志...", "info")
            
            # 创建文件选择对话框
            dialog = Gtk.FileChooserDialog(
                title="保存日志",
                parent=self.get_toplevel(),
                action=Gtk.FileChooserAction.SAVE,
                buttons=(
                    "取消", Gtk.ResponseType.CANCEL,
                    "保存", Gtk.ResponseType.OK
                )
            )
            
            # 设置默认文件名
            current_time = datetime.now().strftime("%Y%m%d_%H%M%S")
            dialog.set_current_name("operation_log_{}.log".format(current_time))
            
            # 添加文件过滤器
            filter_log = Gtk.FileFilter()
            filter_log.set_name("日志文件 (*.log)")
            filter_log.add_pattern("*.log")
            dialog.add_filter(filter_log)
            
            filter_all = Gtk.FileFilter()
            filter_all.set_name("所有文件")
            filter_all.add_pattern("*")
            dialog.add_filter(filter_all)
            
            # 显示对话框
            response = dialog.run()
            
            if response == Gtk.ResponseType.OK:
                filename = dialog.get_filename()
                if not filename.endswith(".log"):
                    filename += ".log"
                    
                # 模拟导出日志
                self.out_dialog.add_output_line("导出日志到: {}".format(filename), "info")
                
                try:
                    # 实际应用中这里会写入文件
                    # with open(filename, "w") as f:
                    #     for line, _ in self.output_lines:
                    #         f.write(line)
                    AuthManager().restore_file(LOG_FILE,filename)
                    os.chmod(filename,0o777)
                    
                    self.out_dialog.add_output_line("日志导出成功", "success")
                    self.update_status("日志已保存到: {}".format(filename))
                except Exception as e:
                    self.out_dialog.add_output_line("日志导出失败: {}".format(str(e)), "error")
            else:
                self.out_dialog.add_output_line("日志导出已取消", "warning")
            
            dialog.destroy()
        
        
        def do_execute(self, err_handle):
            try:
                self.is_running = True
                self.all_config =  self.parent.dump_config()
                print('开始备份')
                self.parent.backup()
                
                steps = len(self.all_config)
                step = 0
                for key,config in self.all_config.items():
                    self.logger.info('============================== {}. {} 任务开始 =============================='.format(step+1, key))
                    self.logger.debug('{} => {}'.format(key,config))
                    
                    # 更新进度

                    if key == 'auth':
                        # 1. auth 配置
                        AuthManager().write_config(config)
                    elif key == 'user':
                        # 2. user 配置
                        UserManager().write_config(config)
                    elif key == 'service':
                        # 3. service 配置   
                        ServiceManager().write_config(config)
                    elif key == 'ssh':
                        # 4. ssh 配置
                        SSHManager().write_config(config)
                    elif key == 'iptables':
                        # 5. iptables 配置
                        IPTablesManager().write_config(config)
                    elif key == 'audit':
                        # 6. audit 配置
                        AuditLogManager().write_config(config)
                    if key == 'other':
                        # 7. grub 配置
                        Other().write_config(config)
                    step += 1
                    progress = step / steps
                    GLib.idle_add( self._update_progress_idle, progress,key, self.execute_btn)
            except Exception as e:
                GLib.idle_add(err_handle,e.__class__,e,e.__traceback__)
                # self.restore_btn.set_sensitive(True)

                
            
        def _update_progress_idle(self, progress, message, execute_btn):
            """在GTK主线程中更新UI"""
            
            # 更新进度条
            self.progress_bar.set_fraction(progress)
            self.progress_bar.set_text("{}%".format(int(progress * 100)))
            self.out_dialog.add_output_line("任务完成：{}".format(message),"info")
            
            print(int(progress*100),'% => ',message)
            # 如果任务完成，重置按钮状态
            if progress >= 1:
                self.is_running = False
                execute_btn.set_sensitive(True)
                self.out_dialog.add_output_line("所有任务执行完成\n", "success")    
                self.update_status("更新配置完成,原配置备份在 /var/log/sectool") 
                self.progress_bar.destroy()
                
                
        def on_import_config(self, widget):
            """ 导入配置 """
            self.out_dialog.add_output_line("开始导出日志...", "info")
            
            # 创建文件选择对话框
            dialog = Gtk.FileChooserDialog(
                title="导入配置",
                parent=self.get_toplevel(),
                action=Gtk.FileChooserAction.SAVE,
                buttons=(
                    "取消", Gtk.ResponseType.CANCEL,
                    "导入", Gtk.ResponseType.OK
                )
            )
            
            # 添加文件过滤器
            filter_log = Gtk.FileFilter()
            filter_log.set_name("日志文件 (*.cfg)")
            filter_log.add_pattern("*.cfg")
            dialog.add_filter(filter_log)
            
            filter_all = Gtk.FileFilter()
            filter_all.set_name("所有文件")
            filter_all.add_pattern("*")
            dialog.add_filter(filter_all)
            
            # 显示对话框
            response = dialog.run()
            
            if response == Gtk.ResponseType.OK:
                filename = dialog.get_filename()
                    
                # 模拟导出日志
                self.out_dialog.add_output_line("导入配置: {}".format(filename), "info")
                
                try:
                    with open(filename ,'r') as f:
                        config = json.load(f)
                        self.parent.update_config(config)
                        self.out_dialog.add_output_line("导入配置成功", "success")
                        self.update_status("导入配置成功")
                except Exception as e:
                    self.out_dialog.add_output_line("导入配置失败: {}".format(str(e)), "error")
            else:
                self.out_dialog.add_output_line("导入配置已取消", "warning")
            
            dialog.destroy()
            
            
        def on_restore_btn(self, widget):
            # 如果已有弹出窗口，先关闭
            if self.popover and self.popover.is_visible():
                self.popover.hide()

            # 创建弹出窗口
            self.popover = Gtk.Popover.new(widget)
            self.popover.set_position(Gtk.PositionType.BOTTOM)
            self.popover.set_size_request(200, 100)
            
            # 创建弹出窗口内容
            popover_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
            self.popover.add(popover_box)
            
            # 标题
            title_label = Gtk.Label(label="选择还原点")
            title_label.get_style_context().add_class("selected-title")
            popover_box.pack_start(title_label, False, False, 5)
            
            # 滚动窗口
            scrolled_window = Gtk.ScrolledWindow()
            scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
            scrolled_window.set_min_content_height(300)
            popover_box.pack_start(scrolled_window, True, True, 0)
            
            # 还原点列表
            list_box = Gtk.ListBox()
            list_box.set_selection_mode(Gtk.SelectionMode.SINGLE)
            list_box.connect("row-activated", self.on_restore_point_selected)
            # list_box.connect("row-activated", lambda box, row : Thread(target=self.on_restore_point_selected, args=(box ,row)).start())
            scrolled_window.add(list_box)
            # 添加还原点到列表
            backup_files = glob.glob('/var/log/sectool/*.bak') 
            for file in backup_files:
                file_name = os.path.basename(file)
                label = Gtk.Label(label=file_name)  # 创建标签控件
                label.set_halign(Gtk.Align.START)  # 或 Gtk.Align.LEFT
                list_box.add(label)                 # 添加到ListBox
            
            # 显示弹出窗口
            self.popover.show_all()


        def on_restore_point_selected(self, list_box, row:Gtk.ListBoxRow):
            """还原点被选中事件"""
            if self.popover:
                self.popover.hide()
            self.restore_btn.set_sensitive(False)
            
            # v4 上多线程会崩溃，目前退回到阻塞状态
            # 创建进度条
            self.progress_bar = Gtk.ProgressBar()
            self.progress_bar.set_show_text(True)
            self.progress_bar.set_fraction(0.0)
            self.status_bar.add(self.progress_bar)
            self.status_bar.show_all()            
            
            file_name = row.get_child().get_text()
            backup_file = os.path.join('/var/log/sectool', file_name)
            self.out_dialog.add_output_line("选中备份: {}".format(file_name), "info")
            Thread(target=self.do_restore, args=(backup_file,self.handle_exception)).start()
                
                
        def do_restore(self, backup_file:str, err_handle):
            try:
                # 执行还原
                backup_infos = {}
                with open(backup_file, 'r') as f:
                    backup_infos = json.load(f)
                
                conf_list = backup_infos.items()
                step = 0
                steps = len(conf_list)
                for key,conf in conf_list:
                    for category in  self.parent.categories:
                        if category.__class__.__name__ == key : 
                            print('正在还原 {}'.format(key))
                            category.restore(conf)
                    step += 1
                    progress = step / steps
                    GLib.idle_add( self._update_progress_idle, progress,key, self.restore_btn)
                    # GLib.idle_add( lambda p,k : self.out_dialog.add_output_line("任务完成：{}".format(k),"info"), progress,key)
                print("还原完成")

            except Exception as e:
                # err_handle(e.__class__,e,e.__traceback__)
                GLib.idle_add(err_handle,e.__class__,e,e.__traceback__)
                # self.restore_btn.set_sensitive(True)
            
            
        # 自定义异常处理器
        def handle_exception(self, exc_type, exc_value, exc_traceback):
            # 忽略键盘中断（Ctrl+C）
            if issubclass(exc_type, KeyboardInterrupt):
                sys.__excepthook__(exc_type, exc_value, exc_traceback)
                return
            
            # 日志记录完整异常信息
            self.logger.error(
                "未捕获的全局异常",
                exc_info=(exc_type, exc_value, exc_traceback)
            )        
            
            # while exc_traceback.tb_next is not None:
            #     exc_traceback = exc_traceback.tb_next
            self.out_dialog.add_output_line('{}:{} => {}'.format(os.path.basename(exc_traceback.tb_frame.f_globals['__file__'])
                                                    ,exc_traceback.tb_lineno,exc_value),'error') 
            self.err_dialog.show_exception(exc_type,exc_value, exc_traceback)

    gi.require_version('Gtk', '3.0')

    # 主窗口类
    class MainWindow(Gtk.Window):
        
        app_name = "系统加固工具"
        
        def __init__(self, categories):
            # 所有的配置项
            super().__init__(title=self.app_name)
            self.logger = logging.getLogger(__name__)
            self.set_default_size(800, 600)
            self.set_icon_name('preferences-system')

            # 界面布局
            self.categories = categories
            finished_ui = DoTaskWidget(self)
            self.categories.append(finished_ui)
            
            # 初始化数据
            self.data_widgets = {}

            # 创建主布局
            self.main_paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL)
            self.add(self.main_paned)

            # 左侧分类列表
            self.category_list = Gtk.ListBox()
            self.category_list.set_selection_mode(Gtk.SelectionMode.SINGLE)
            self.category_list.connect("row-selected", self.on_category_selected)
            self.main_paned.add1(self.category_list)
            self.main_paned.set_position(110)
            
            # 右侧配置堆栈
            self.config_stack = Gtk.Stack()
            self.config_stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
            self.main_paned.add2(self.config_stack)
            
            # 初始化界面
            self.build_category_list()
            self.build_config_pages()
            
            # 默认选中第一个分类
            self.category_list.select_row(self.category_list.get_row_at_index(0))
            
            # Ctrl+Alt+C 触发关闭窗口
            self.connect("key-press-event", self.on_key_press)
            
        
        def on_key_press(self, widget, event):
            if (event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.MOD1_MASK) == 
                (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.MOD1_MASK) and
                event.keyval == Gdk.KEY_c):
                print("Ctrl+Alt+C 触发关闭窗口")
                self.destroy()
        
            
        # 构建左侧分类列表
        def build_category_list(self):
            for category in self.categories:
                self.logger.debug(category)
                ui_config = category.ui_config
                row = Gtk.ListBoxRow()
                row.set_margin_top(3)
                row.set_margin_bottom(3)
                box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
                row.add(box)
                
                icon = Gtk.Image.new_from_icon_name("folder-symbolic", Gtk.IconSize.BUTTON)
                icon.set_margin_start(10)       # 增加左侧空隙，更美观
                label = Gtk.Label(label=ui_config.get('category'), xalign=0)
                
                box.pack_start(icon, False, False, 0)
                box.pack_start(label, True, True, 0)
                self.category_list.add(row)
                
        
        # 构建右侧配置页面
        def build_config_pages(self):
            for category in self.categories:
                ui_config = category.ui_config
                # 创建单个配置页面
                scroll = Gtk.ScrolledWindow()
                viewport = Gtk.Viewport()
                scroll.add(viewport)
                id = ui_config.get('id')
                self.config_stack.add_titled(scroll, id , ui_config.get('category'))
                
                container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
                container.set_margin_top(20)
                container.set_margin_bottom(20)
                container.set_margin_start(20)
                container.set_margin_end(20)
                viewport.add(container)

                data_ui = []
                
                # 动态生成配置项控件
                for key,item in ui_config['uis'].items():
                    
                    label, widget_type = item[0], item[1]
                    
                    custom_label = Gtk.Label()
                    custom_label.set_markup("<b>{}</b>".format(label))
                    # frame = Gtk.Frame(label=label)
                    frame = Gtk.Frame()
                    frame.set_label_widget(custom_label)
                    # frame.set_border_width(20)
                    frame.set_shadow_type(Gtk.ShadowType.IN)
                    content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
                    content_box.set_margin_top(20)
                    frame.add(content_box)
                    
                    # 根据类型创建不同控件
                    if widget_type == "entry":
                        entry = Gtk.Entry()
                        entry.set_name(key)
                        defaul_text = str(item[2]) if item[2] else ''
                        entry.set_text(defaul_text)
                        content_box.pack_start(entry, True, True, 0)
                        data_ui.append(entry)
                        
                    elif widget_type == "spin":
                        adj = Gtk.Adjustment(value=item[2], lower=0, upper=65535, step_increment=1)
                        spin = Gtk.SpinButton(digits=0, adjustment=adj)
                        spin.set_name(key)
                        content_box.pack_start(spin, True, True, 0)
                        data_ui.append(spin)

                    elif widget_type == "check":
                        check = Gtk.CheckButton(label="Enable")
                        check.set_name(key)
                        check.set_active(item[2])
                        content_box.pack_start(check, True, True, 0)
                        data_ui.append(check)
                        
                    elif widget_type == "combo":
                        combo = Gtk.ComboBoxText()
                        combo.set_name(key)
                        for option in item[2]:
                            combo.append_text(option)
                        combo.set_active(item[2].index(item[3]) if len(item) > 3 else 0)
                        content_box.pack_start(combo, True, True, 0)
                        data_ui.append(combo)
                        
                    elif widget_type == "scale":
                        min_val, max_val, step = item[2]
                        adj = Gtk.Adjustment(value=item[3], lower=min_val, upper=max_val, step_increment=step)
                        scale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, adjustment=adj)
                        scale.set_name(key)
                        scale.set_digits(0)
                        content_box.pack_start(scale, True, True, 0)
                        data_ui.append(scale)
                        
                    elif widget_type == 'textview':
                        # 创建滚动窗口
                        scrolled_window = Gtk.ScrolledWindow()
                        scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
                        scrolled_window.set_size_request(400, 400)
                        scrolled_window.set_margin_top(10)
                        # 创建文本视图
                        text_view = Gtk.TextView()
                        # text_view.set_cursor_visible(False)
                        text_view.set_wrap_mode(Gtk.WrapMode.WORD)
                        text_view.get_buffer().set_text(item[2])
                        scrolled_window.add(text_view)
                        text_view.set_name(key)
                        
                        # text_view.override_background_color(
                        #     Gtk.StateFlags.NORMAL,
                        #     Gdk.RGBA(0xE7/255.0, 0xE7/255.0, 0xE7/255.0, 1.0)  # 灰色 (R,G,B,A) # E7E7E7
                        # )
                        
                        style_context = text_view.get_style_context()
                        # 创建 CSS 提供器并加载样式
                        css_provider = Gtk.CssProvider()
                        css_provider.load_from_data("""
                            textview {
                                background: #e7e7e7;
                                padding: 10px;
                            }
                            textview text {
                                color: rgba(255,0,0,1);
                                background: transparent
                            }
                        """.encode('utf-8'))
                        # 将样式应用到 TextView
                        style_context.add_provider(
                            css_provider,
                            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
                        )

                        data_ui.append(text_view)
                        content_box.pack_start(scrolled_window, True, True ,0)
                        
                    elif widget_type == 'scripteditor':
                        script_editor = ScriptEditor(self)
                        textview = script_editor.textview
                        textview.get_buffer().set_text(item[2])
                        textview.set_name(key)
                        content_box.pack_start(script_editor, True, True ,0)
                        data_ui.append(textview)
                        
                    elif widget_type == "table":
                        self.logger.debug(key)
                        table = TableViewer( ui_config, key)
                        table.set_name(key)
                        if hasattr(category, 'get_row_data'):
                            table.get_row_data = category.get_row_data
                        content_box.pack_start(table, True, True, 0)
                        data_ui.append(table)
                    
                    elif widget_type == "user_table":
                        self.logger.debug(key)
                        table = UserTable( ui_config, key)
                        table.set_name(key)
                        if hasattr(category, 'get_row_data'):
                            table.get_row_data = category.get_row_data
                        content_box.pack_start(table, True, True, 0)
                        data_ui.append(table)
                        
                    elif widget_type == 'dotask':
                        dotask = DoTaskWidget(self)
                        content_box.pack_start(dotask, True, True, 0)
                        
                    container.pack_start(frame, False, False, 0)
                    # 添加下划线
                    separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
                    container.pack_start(separator, False, False, 0)
                    
                if not id.startswith('__'):
                    self.data_widgets.update({ id: data_ui})
            return True
        
        # 分类选择事件
        def on_category_selected(self, listbox, row):
            if row is not None:
                index = row.get_index()
                self.config_stack.set_visible_child_name(self.categories[index].ui_config.get('id'))


        def dump_config(self):
            all_config = {}
            self.logger.debug("data_widgets = {}".format(self.data_widgets))
            for key,uis in  self.data_widgets.items():
                if key.startswith('__'): continue
                temp_list = {}
                for ui in uis:                
                    # 根据类型取值
                    widget_type = type(ui)
                    self.logger.debug(widget_type)
                    if widget_type == Gtk.Entry:
                        data = ui.get_text()
                        if key == 'user':
                            data = data.split(',')
                        
                    elif widget_type == Gtk.SpinButton:
                        data = int(ui.get_value_as_int())

                    elif widget_type == Gtk.CheckButton:
                        data = ui.get_active()
                        
                    elif widget_type == Gtk.ComboBoxText:
                        data = ui.get_active_text()
                    
                    elif widget_type == Gtk.Scale:
                        data = int(ui.get_value())
                    
                    elif widget_type == Gtk.TextView:
                        # 获取文本缓冲区的开始和结束迭代器
                        buffer = ui.get_buffer()
                        start_iter = buffer.get_start_iter()
                        end_iter = buffer.get_end_iter()
                        # 获取两个迭代器之间的文本
                        data = buffer.get_text(start_iter, end_iter, True)
                        
                    elif issubclass(widget_type, TableViewer):
                        data = ui.on_save_data(self)
                        if ui.get_name() == 'disable':
                           data = [ item['unit'] for item in data ] 
            
                    temp_list.update( {ui.get_name():data} )
                all_config.update( {key: temp_list} )
                
            self.logger.debug(all_config)
            return all_config



        def update_config(self, all_config: dict) -> bool:
            try :
                self.logger.debug(all_config)
                for category,config in all_config.items():
                    uis = self.data_widgets.get(category)
                    self.logger.debug('{} => {}'.format(category,config))
                    for ui in uis:    
                        key = ui.get_name()
                        data = config.get(key)
                        self.logger.debug('{} => {}'.format(key,data))
                        if not data : continue
                        
                        # 根据类型设值
                        widget_type = type(ui)
                        if widget_type == Gtk.Entry:
                            if isinstance(data,list):
                                data = ','.join(data)
                            ui.set_text(data)
                            
                        elif widget_type == Gtk.SpinButton:
                            ui.set_value(int(data))

                        elif widget_type == Gtk.CheckButton:
                            if data == True or data.upper() == 'YES' or data.upper() == 'TRUE':
                                data=True
                            else:
                                data=False
                            ui.set_active(data)
                            
                        elif widget_type == Gtk.ComboBoxText:
                            ui.set_active_id(data)
                        
                        elif widget_type == Gtk.Scale:
                            ui.set_value(int(data))

                        elif widget_type == Gtk.TextView:
                            ui.get_buffer().set_text(str(data))
                            
                        elif issubclass(widget_type, TableViewer):
                            ui.on_update_data(data)
                return True
            except Exception as e:
                self.logger.error(e)
                self.logger.error('错误：{}: {}'.format(e.__traceback__.tb_frame.f_globals['__file__'], e.__traceback__.tb_lineno))
                raise
        
        def backup(self):
            backup_infos = {}
            suffix = datetime.now().strftime("%Y%m%d%H%M%S")
            for category in  self.categories:
                if category.__class__.__name__ == 'DoTaskWidget': continue
                info = category.backup(suffix)
                backup_infos.update( info )
            backup_file = os.path.join("/var/log/sectool/", "{}.bak".format(suffix) )
            with open(backup_file, 'w') as f:
                json.dump(backup_infos,f,indent=4)
            
        def restore(self, backup_file:str):
            backup_infos = {}
            with open(backup_file, 'r') as f:
                backup_infos = json.load(f)
            
            for key,conf in backup_infos.items():
                for category in  self.categories:
                    if category.__class__.__name__ == key : 
                        print('正在还原 {}'.format(key))
                        category.restore(conf)
            print("还原完成")
            return True
                
        def load_global_css(self):
            """加载全局 CSS 样式"""
            variables = {
                'primary_color': '#4a8cff',
                'secondary_color': '#6a6cff',
                'error_color': '#ff6b6b',
                'success_color': '#4caf50',
                'warning_color': '#ff9800',
                'gray_color': '#e7e7e7',
                'text_color': '#333333',
                'bg_color': '#f8f8f8',
                'border_radius': '4px',
                'padding': '8px',
                'shadow': '0 2px 6px rgba(0, 0, 0, 0.1)'
            }
            
            # 基础 CSS 模板
            template = string.Template("""
            /* 主窗口样式 */
            window {
                background-color: ${bg_color};
                color: ${text_color};
                font-family: 'Sans';
            }
            
            /* 按钮样式 */
            button {
                border: 1px solid #e2e8f0;
                padding: ${padding};
                font-weight: bold;
                transition: all 0.2s ease;
            }
            
            button:hover {
                background-color: ${secondary_color};
                box-shadow: ${shadow};
            }
            
            /* 输入框样式 */
            entry {
                padding: ${padding};
                border-radius: ${border_radius};
                border: 1px solid #ddd;
                background-color: white;
            }
            
            entry:focus {
                border-color: ${primary_color};
                box-shadow: 0 0 0 2px ${primary_color}30;
            }
            
            /* 错误标签 */
            .error-label {
                color: ${error_color};
                font-weight: bold;
            }
            
            /* 成功标签 */
            .success-label {
                color: ${success_color};
                font-weight: bold;
            }
                    
            /* 文本框样式 */
            textview {
                background: ${gray_color}; 
                padding: 10px;
                border: 3px solid #ddd; border: 1px solid #ddd;
                border-color: ${primary_color};
                border-radius: 4px;
            }
            textview text {
                color: rgba(255,0,0,1);
                background: transparent
            }
            """)
            
            # 太丑，禁用了
            # css = template.substitute(variables)
            # provider = Gtk.CssProvider()
            # provider.load_from_data(css.encode())
            
            # screen = Gdk.Screen.get_default()
            # style_context = Gtk.StyleContext()
            # style_context.add_provider_for_screen(
            #     screen, 
            #     provider, 
            #     Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
            # )
        
        def run(self):
            self.load_global_css()
            self.connect("destroy", Gtk.main_quit)
            self.show_all()
            Gtk.main()

    gi.require_version('Gtk', '3.0')


    class TableViewer(Gtk.Frame):
        
        class EditableCellRendererCombo(Gtk.CellRendererCombo):
            __gtype_name__ = 'EditableCellRendererCombo'
            
            def __init__(self, **kwargs):
                super().__init__(**kwargs)
                self.set_property("editable", True)
                # self.set_property("has-entry", False) # 下拉框选择也可以自己输入
                
            
        def __init__(self, ui_config:dict, key:str):
            super().__init__()
            self.logger = logging.getLogger(__name__)
            self.set_size_request(400, 400)
            
            # 从 ui_config 中获取 UI
            self.logger.debug(ui_config)
            data = ui_config.get('{}_data'.format(key))
            columns = ui_config.get('{}_columns'.format(key))
            combo_columns = ui_config.get('{}_combo_columns'.format(key))
            tip_columns = ui_config.get('{}_tips'.format(key))
            default_columns = ui_config.get('{}_defaults'.format(key))
                        
            self.logger.debug('param: data= {}'.format(data))
            self.logger.debug('param: columns= {}'.format(columns))
            self.logger.debug('param: combo_columns= {}'.format(combo_columns))
            self.logger.debug('param: default_columns= {}'.format(default_columns))
            
            # 根据传值情况设置到实例属性
            self.default_columns = default_columns  
            self.combo_columns = combo_columns if combo_columns else {}  
            
            if data:
                self.data = [ data ] if isinstance(data,dict) else data
                if not columns:
                    columns = []
                    for key,_ in self.data[0].items():
                        if not key.startswith('__'):
                            columns.append(key)
            else:
                self.data = []
            if not columns:
                columns = ["列 {}".format(i+1) for i in range(len(data[0]))]           
            if isinstance(columns,dict):
                self.headers = columns
            else:
                self.headers = { key : None for key in columns }
        
            self.header_types =  [str] * len(self.headers)   
        
            self.setup_ui()       
            
            # 增加提示
            if tip_columns:
                self.logger.debug('param: tip_columns= {}'.format(tip_columns))
                self.tip_columns = tip_columns
                self.treeview.set_has_tooltip(True)
                self.treeview.connect("query-tooltip", self.on_query_tooltip)
            
            
        def setup_ui(self):
            main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
            self.add(main_box)
            
            # 工具栏
            # toolbar = Gtk.Toolbar()
            # main_box.pack_end(toolbar, False, False, 0)
            
            # # add_btn = Gtk.ToolButton.new_from_stock(Gtk.STOCK_ADD) # 过时
            # add_btn = Gtk.ToolButton()
            # add_btn.set_icon_name("list-add")  # 设置图标名称
            # add_btn.connect("clicked", self.on_add_row)
            # toolbar.insert(add_btn, 0)
            
            # # save_btn = Gtk.ToolButton.new_from_stock(Gtk.STOCK_SAVE) # 过时
            # save_btn = Gtk.ToolButton()
            # save_btn.set_icon_name("document-save")  # 设置图标名称
            # save_btn.connect("clicked", self.on_save_data)
            # toolbar.insert(save_btn, 1)
            
            # 创建表格
            self.create_table()
            
            # 滚动窗口
            scrolled_window = Gtk.ScrolledWindow()
            scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
            scrolled_window.add(self.treeview)
            main_box.pack_start(scrolled_window, True, True, 0)
            
            # 状态栏
            # self.statusbar = Gtk.Statusbar()
            # main_box.pack_end(self.statusbar, False, False, 0)
            
            # 创建右键菜单
            # self.create_context_menu()
            
            # 显示所有组件
            self.show_all()
        
        def create_table(self):
            # 创建ListStore - 根据列数动态生成类型
            self.liststore = Gtk.ListStore(*self.header_types)
            
            # 添加数据
            for row in self.data:
                self.logger.debug(row)
                line = []
                for key in self.headers.keys():
                    if key.startswith('__'): continue
                    
                    value = row.get(key,'')
                    # bool 值需要特殊处理
                    if isinstance(value,bool):
                        value = str(value)
                    if value == '' :
                        if self.default_columns:
                            value = self.default_columns.get(key)
                        else:
                            value = ''
                        
                    if isinstance(value, list): 
                        value = ','.join(value)
                    else:
                        value = str(value)
                    line.append(value)
                self.liststore.append(line)
            
            # 创建TreeView
            self.treeview = Gtk.TreeView(model=self.liststore)
            self.treeview.set_grid_lines(Gtk.TreeViewGridLines.BOTH)
            
            # 创建列
            for i, header_kv in enumerate(self.headers.items()):
                self.logger.debug('{} => {}'.format(i,header_kv))
                if header_kv[1]: 
                    header = header_kv[1]
                else:
                    header = header_kv[0]
                if header_kv[0] in self.combo_columns:
                    # 下拉框列
                    renderer = self.EditableCellRendererCombo()
                    # renderer.set_property("xalign", 0.5)  # 单元格内容居中
                    
                    # 创建下拉框数据模型
                    combo_list = Gtk.ListStore(str)
                    self.logger.debug('{} => {}'.format(header_kv[0],self.combo_columns[header_kv[0]]))
                    for item in self.combo_columns[header_kv[0]]:
                        combo_list.append([item])
                    
                    renderer.set_property("model", combo_list)
                    renderer.set_property("text-column", 0)
                    renderer.connect("edited", self.on_cell_edited, i)

                else:
                    # 普通文本列
                    renderer = Gtk.CellRendererText()
                    renderer.set_property("editable", True)
                    renderer.connect("edited", self.on_cell_edited, i)
                    
                column = Gtk.TreeViewColumn(header, renderer, text=i)
                column.set_resizable(True)
                column.set_min_width(50)
                column.set_alignment(0.5)  # 列标题居中
                column.set_expand(True)   #  自动列宽
                self.treeview.append_column(column)
            # 连接双击事件
            self.button_press_event_id = self.treeview.connect("button-press-event", self.on_treeview_button_press)
        
        def create_context_menu(self):
            self.context_menu = Gtk.Menu()

            # 删除行
            delete_item = Gtk.MenuItem(label="删除行")
            delete_item.connect("activate", self.on_delete_row)
            
            self.context_menu.append(delete_item)
            
            # 复制行
            copy_item = Gtk.MenuItem(label="复制行")
            copy_item.connect("activate", self.on_copy_row)
            self.context_menu.append(copy_item)
            
            # 有选择才显示
            selection = self.treeview.get_selection()
            model, treeiter = selection.get_selected()
            if not treeiter :
                delete_item.set_sensitive(False)
                copy_item.set_sensitive(False)
                
            # 分隔线
            self.context_menu.append(Gtk.SeparatorMenuItem())
            
            # 添加行
            add_item = Gtk.MenuItem(label="添加行")
            add_item.connect("activate", self.on_add_row)
            self.context_menu.append(add_item)
            
            self.context_menu.show_all()
            
        
        def on_treeview_button_press(self, widget, event):
            # 右键点击显示菜单
            if event.button == 3:  # 右键
                self.create_context_menu()
                # 获取点击位置
                path_info = widget.get_path_at_pos(int(event.x), int(event.y))
                if path_info:
                    path, col, cell_x, cell_y = path_info
                    widget.grab_focus()
                    widget.set_cursor(path, col, 0)
                self.context_menu.popup(None, None, None, None, event.button, event.time)
                return True
            return False
        
        
        def on_cell_edited(self, widget, path, new_text, column_index):
            ''' 编辑完成后处理: path 行号, column_index 列号， new_text 值'''
            self.liststore[path][column_index] = new_text
            print("单元格已更新: 列 {} = {}".format(column_index,new_text))
            # self.statusbar.push(0, "单元格已更新: 列 {} = {}".format(column_index+1,new_text))
            if column_index == 0 and hasattr(self, 'get_row_data'): 
                values = self.get_row_data(new_text)
                for i, key in enumerate(self.headers.keys()):            
                    self.liststore[path][i] = values.get(key)
        
        def on_delete_row(self, widget):
            selection = self.treeview.get_selection()
            model, treeiter = selection.get_selected()
            if treeiter:
                row_index = model.get_path(treeiter)[0]
                model.remove(treeiter)
                # self.statusbar.push(0, "已删除行 {}".format(row_index+1))
        
        def on_copy_row(self, widget):
            selection = self.treeview.get_selection()
            model, treeiter = selection.get_selected()
            if treeiter:
                row_data = model[treeiter][:]
                new_iter = model.append(row_data)
                # self.statusbar.push(0, "已复制行")
        
        def on_add_row(self, widget=None):
            # 添加空行
            if self.default_columns:
                new_row = []
                for header in self.headers:
                    new_row.append( self.default_columns.get(header) )
            else:
                new_row = [""] * len(self.headers)
            self.liststore.append(new_row)
            # self.statusbar.push(0, "已添加新行")
        
        def on_save_data(self, widget):
            # self.statusbar.push(0, "数据已保存！")
            # 打印当前数据
            self.logger.debug("\n当前表格数据:")
            self.logger.debug(" | ".join(self.headers))
            for row in self.liststore:
                self.logger.debug(" | ".join(row))
            
            data_list = []
            headers =  list(self.headers.keys())
            for row in self.liststore:
                temp_data ={}
                if not row[0]: continue # 第一列为空时，跳过
                for key,value in  enumerate(row) :
                    self.logger.debug("{} => {}".format(key,self.headers))
                    key = headers[key]
                    if value and value != 'None':
                        temp_data.update( { key:value } )
                if temp_data:
                    data_list.append(temp_data)
            return data_list
        
        
        def on_update_data(self, data):
            # 导入数据时使用
            self.logger.debug(data)
            self.liststore.clear()
            
            # 修复服务界面导入数据异常
            if hasattr(self, 'get_row_data'):
                new_data = [] 
                for row in data:
                    line = self.get_row_data(row)
                    new_data.append(line)
                data = new_data
                    
            for row in data:
                line = []
                for key in self.headers.keys():
                    if key.startswith('__'): continue
                    if isinstance(row,dict):
                        value = row.get(key,'')
                    else:
                        value = row
                        
                    if not value:
                        value = ''
                        
                    if isinstance(value, list): 
                        value = ','.join(value)
                    else:
                        value = str(value)
                    line.append(value)
                self.liststore.append(line)
                

        def on_query_tooltip(self, widget, x, y, keyboard_mode, tooltip):
            """工具提示查询处理函数"""
            # 获取鼠标位置对应的路径
            bin_x, bin_y = widget.convert_widget_to_bin_window_coords(x, y)
            path_info = widget.get_path_at_pos(bin_x, bin_y)    
            if path_info:
                # 获取模型迭代器
                path, column, cell_x, cell_y = path_info
                # print(path.get_indices(), column, cell_x, cell_y)
                # 设置工具提示文本
                tip_text = self.tip_columns.get(column.get_title())
                if  tip_text:
                    tooltip.set_markup(tip_text)
                    return True  # 表示已处理工具提示
            
            return False  # 表示未处理工具提示
                


    class UserTable(TableViewer):
        
        def __init__(self,  ui_config:dict, key:str):
            super().__init__(ui_config , key)
            if  self.button_press_event_id :
                self.treeview.disconnect(self.button_press_event_id )    


        

    gi.require_version('Gtk', '3.0')

    class ScriptEditor(Gtk.Frame):
        '''
        下面是 Dialog 旧 API 参数的替换
        旧参数名        新参数名        用途
        type            message_type    指定对话框类型（信息/警告等）
        message_format  text            设置主消息文本
        parent          transient_for   关联父窗口
        flags=Gtk.DialogFlags.MODAL                   modal=True                 设置对话框为模态（阻止与其他窗口交互）
        flags=Gtk.DialogFlags.DESTROY_WITH_PARENT     destroy_with_parent=True   当父窗口关闭时自动销毁对话框
        # destroy_with_parent=True 和 modal=True 二选一
        '''
        def __init__(self, parent):        
            super().__init__()
            self.parent = parent
            self.set_size_request(400,400)
            self.data ={
                'import': "导入脚本",
                'export': "导出脚本",
                'check': "检查语法",
                'execute': "执行脚本"
            }
            
            # Create main vertical box
            self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
            self.add(self.vbox)
            
            # 创建菜单栏
            # self.create_menu_bar()
            # 创建工具栏
            self.create_toolbar()
            
            # 创建滚动条包裹文本框
            scrolled_window = Gtk.ScrolledWindow()
            scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
            self.vbox.pack_start(scrolled_window, True, True, 0)
            self.textview = Gtk.TextView()
            self.textview.set_wrap_mode(Gtk.WrapMode.WORD)
            self.textbuffer = self.textview.get_buffer()
            scrolled_window.add(self.textview)
            
            # 创建状态栏
            self.statusbar = Gtk.Statusbar()
            self.context_id = self.statusbar.get_context_id("status")
            self.vbox.pack_end(self.statusbar, False, True, 0)
            
            # 创建右键菜单
            # self.create_context_menu()
            # 添加快捷键
            # self.add_accelerators()
            
            # 信号绑定
            self.textview.connect("button-press-event", self.on_textview_button_press)
            
            # 初始状态
            self.update_status("就绪")
            self.show_all()
        
        def create_menu_bar(self):
            menubar = Gtk.MenuBar()
            self.vbox.pack_start(menubar, False, False, 0)
            
            # File menu
            file_menu = Gtk.Menu()
            file_item = Gtk.MenuItem(label="File")
            file_item.set_submenu(file_menu)
            
            # File menu items
            open_item = Gtk.MenuItem(label="Open")
            open_item.connect("activate", self.on_open)
            file_menu.append(open_item)
            
            save_item = Gtk.MenuItem(label="Save")
            save_item.connect("activate", self.on_save)
            file_menu.append(save_item)
            
            file_menu.append(Gtk.SeparatorMenuItem())
            
            quit_item = Gtk.MenuItem(label="Quit")
            quit_item.connect("activate", Gtk.main_quit)
            file_menu.append(quit_item)
            
            # Edit menu
            edit_menu = Gtk.Menu()
            edit_item = Gtk.MenuItem(label="Edit")
            edit_item.set_submenu(edit_menu)
            
            # Edit menu items
            check_item = Gtk.MenuItem(label="Check Syntax")
            check_item.connect("activate", self.on_check_syntax)
            edit_menu.append(check_item)
            
            run_item = Gtk.MenuItem(label="Run Script")
            run_item.connect("activate", self.on_run_script)
            edit_menu.append(run_item)
            
            menubar.append(file_item)
            menubar.append(edit_item)
        
        
        def create_toolbar(self):
            toolbar = Gtk.Toolbar()
            self.vbox.pack_start(toolbar, False, False, 0)
            
            # Open button
            # open_btn = Gtk.ToolButton.new_from_stock(Gtk.STOCK_OPEN)
            open_btn = Gtk.ToolButton()
            open_btn.set_icon_name("document-open")   # 设置图标名称
            open_btn.connect("clicked", self.on_open)
            toolbar.insert(open_btn, 0)
            
            # Save button
            save_btn = Gtk.ToolButton()
            save_btn.set_icon_name("document-save")   # 设置图标名称
            save_btn.connect("clicked", self.on_save)
            toolbar.insert(save_btn, 1)
            
            toolbar.insert(Gtk.SeparatorToolItem(), 2)
            
            # Check syntax button
            check_btn = Gtk.ToolButton()
            check_btn.set_icon_name("tools-check-spelling")   # 设置图标名称
            check_btn.set_tooltip_text(self.data.get('check'))
            check_btn.connect("clicked", self.on_check_syntax)
            toolbar.insert(check_btn, 3)
            
            # Run button
            run_btn = Gtk.ToolButton()
            run_btn.set_icon_name('media-playback-start')
            run_btn.set_tooltip_text(self.data.get('execute'))
            run_btn.connect("clicked", self.on_run_script)
            toolbar.insert(run_btn, 4)
        
        
        def create_context_menu(self):
            self.context_menu = Gtk.Menu()
            
            # Menu items
            open_item = Gtk.MenuItem(label=self.data.get('import'))
            open_item.connect("activate", self.on_open)
            self.context_menu.append(open_item)
            
            save_item = Gtk.MenuItem(label=self.data.get('export'))
            save_item.connect("activate", self.on_save)
            self.context_menu.append(save_item)
            
            self.context_menu.append(Gtk.SeparatorMenuItem())
            
            check_item = Gtk.MenuItem(label=self.data.get('check'))
            check_item.connect("activate", self.on_check_syntax)
            self.context_menu.append(check_item)
            
            run_item = Gtk.MenuItem(label=self.data.get('execute'))
            run_item.connect("activate", self.on_run_script)
            self.context_menu.append(run_item)
            
            self.context_menu.show_all()
        
        
        def add_accelerators(self):
            # Create accelerator group
            accel_group = Gtk.AccelGroup()
            # self.parent.add_accel_group(accel_group)
            self.parent.get_toplevel().add_accel_group(accel_group)
            
            # Add keyboard shortcuts
            open_item = self.find_menu_item(self.context_menu, self.data.get('import'))
            open_item.add_accelerator("activate", accel_group, 
                                    Gdk.KEY_o, Gdk.ModifierType.CONTROL_MASK, 
                                    Gtk.AccelFlags.VISIBLE)
            
            save_item = self.find_menu_item(self.context_menu, self.data.get('export'))
            save_item.add_accelerator("activate", accel_group, 
                                    Gdk.KEY_s, Gdk.ModifierType.CONTROL_MASK, 
                                    Gtk.AccelFlags.VISIBLE)
            
            check_item = self.find_menu_item(self.context_menu, self.data.get('check'))
            check_item.add_accelerator("activate", accel_group, 
                                    Gdk.KEY_c, Gdk.ModifierType.CONTROL_MASK, 
                                    Gtk.AccelFlags.VISIBLE)
            
            run_item = self.find_menu_item(self.context_menu, self.data.get('execute'))
            run_item.add_accelerator("activate", accel_group, 
                                    Gdk.KEY_r, Gdk.ModifierType.CONTROL_MASK, 
                                    Gtk.AccelFlags.VISIBLE)
        
        def find_menu_item(self, menu, label):
            for item in menu.get_children():
                if isinstance(item, Gtk.MenuItem) and item.get_label() == label:
                    return item
            return None
        
        def on_textview_button_press(self, widget, event):
            # Check if right mouse button was pressed
            if event.button == 3:  # Right click
                self.context_menu.popup(None, None, None, None, event.button, event.time)
                return True
            return False
        
        def on_open(self, widget):
            dialog = Gtk.FileChooserDialog(
                title="选择要导入的脚本", 
                parent=self.parent,
                action=Gtk.FileChooserAction.OPEN
            )
            dialog.add_buttons(
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OPEN, Gtk.ResponseType.OK
            )
            
            # Add Python file filter
            filter_script = Gtk.FileFilter()
            filter_script.set_name("Script files (*.py|.sh)")
            filter_script.add_pattern("*.py")
            filter_script.add_pattern("*.sh")
            dialog.add_filter(filter_script)
            
            # Add all files filter
            filter_any = Gtk.FileFilter()
            filter_any.set_name("All files")
            filter_any.add_pattern("*")
            dialog.add_filter(filter_any)
            
            response = dialog.run()
            if response == Gtk.ResponseType.OK:
                filename = dialog.get_filename()
                try:
                    with open(filename, 'r') as file:
                        content = file.read()
                        self.textbuffer.set_text(content)
                    self.update_status("载入文件: {}".format(filename))
                except Exception as e:
                    self.show_error("载入文件失败: {}".format(str(e)))
            
            dialog.destroy()
        
        def on_save(self, widget):
            dialog = Gtk.FileChooserDialog(
                title="导出脚本文件", 
                parent=self.parent,
                action=Gtk.FileChooserAction.SAVE
            )
            dialog.add_buttons(
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_SAVE, Gtk.ResponseType.OK
            )
            dialog.set_do_overwrite_confirmation(True)
            dialog.set_current_name("script.py")
            
            response = dialog.run()
            if response == Gtk.ResponseType.OK:
                filename = dialog.get_filename()
                # if not filename.endswith('.py'):
                #     filename += '.py'
                    
                start_iter = self.textbuffer.get_start_iter()
                end_iter = self.textbuffer.get_end_iter()
                content = self.textbuffer.get_text(start_iter, end_iter, True)
                
                try:
                    with open(filename, 'w') as file:
                        file.write(content)
                    os.chmod(filename,0o777)
                    self.update_status("保存到: {}".format(filename))
                except Exception as e:
                    self.show_error("保存文件失败: {}".format(str(e)))
            
            dialog.destroy()
        
        
        def on_check_syntax(self, widget):
            start_iter = self.textbuffer.get_start_iter()
            end_iter = self.textbuffer.get_end_iter()
            code = self.textbuffer.get_text(start_iter, end_iter, True)
            
            shell_bang = '#!/bin/bash'
            for line in code.split('\n'):
                line = line.strip()
                if line.startswith('#!'): 
                    shell_bang = line
                    break
            shell_bin = shell_bang.split('!')[1]
            
            # Create a temporary file to check syntax
            with tempfile.NamedTemporaryFile(suffix='.py', delete=False) as tmpfile:
                tmpfile.write(code.encode('utf-8'))
                tmpfile_path = tmpfile.name
            
            try:
                if 'sh' in shell_bin:
                    cmd = [ shell_bin, '-n', tmpfile_path]
                elif 'python' in shell_bin:
                # Use the system Python to check syntax
                    cmd = [ shell_bin, '-m', 'py_compile', tmpfile_path]
                else:
                    raise Exception('不支持的脚本类型：{}'.format(shell_bin))
                
                result = subprocess.run(
                    cmd,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    # text=True
                    universal_newlines=True
                )
                
                if result.returncode == 0:
                    self.update_status("{}: OK".format(self.data.get('check')))
                    self.show_info(self.data.get('check'), "没有语法错误!")
                else:
                    error_msg = result.stderr
                    self.update_status("{}: Failed".format(self.data.get('check')))
                    self.show_error("语法错误:\n{}".format(error_msg))
                    
                    # Try to extract line number from error message
                    if "line " in error_msg:
                        line_part = error_msg.split("line ")[1]
                        if line_part and line_part[0].isdigit():
                            line_num = int(line_part.split()[0])
                            self.highlight_line(line_num)
            
            except Exception as e:
                raise
                # self.show_error("执行出错: {}".format(str(e)))
            finally:
                os.unlink(tmpfile_path)
        
        def on_run_script(self, widget):
            start_iter = self.textbuffer.get_start_iter()
            end_iter = self.textbuffer.get_end_iter()
            code = self.textbuffer.get_text(start_iter, end_iter, True)
            
            shell_bang = '#!/bin/bash'
            for line in code.split('\n'):
                line = line.strip()
                if line.startswith('#!'): 
                    shell_bang = line
                    break
            shell_bin = shell_bang.split('!')[1]
            
            # Create a temporary file to run
            with tempfile.NamedTemporaryFile(suffix='.tmp', delete=False, mode='w') as tmpfile:
                tmpfile.write(code)
                tmpfile_path = tmpfile.name
            
            try:
                # Run the script and capture output
                if 'sh' in shell_bin:
                    cmd = [ shell_bin, tmpfile_path]
                elif 'python' in shell_bin:
                # Use the system Python to check syntax
                    cmd = [ shell_bin, tmpfile_path]
                else:
                    raise Exception('不支持的脚本类型：{}'.format(shell_bin))
                
                result = subprocess.run(
                    cmd,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    # text=True
                    universal_newlines=True
                )
                
                output = result.stdout if result.stdout else ""
                errors = result.stderr if result.stderr else ""
                
                if result.returncode == 0:
                    self.update_status("执行成功")
                    self.show_output("执行输出", output)
                else:
                    self.update_status("执行失败")
                    self.show_output("执行输出", "code: {}\n\n{}\n{}".format(result.returncode,output,errors))
                    
                    # Try to extract line number from error message
                    if "line " in errors:
                        line_part = errors.split("line ")[1]
                        if line_part and line_part[0].isdigit():
                            line_num = int(line_part.split()[0])
                            self.highlight_line(line_num)
            
            except Exception as e:
                self.show_error("执行出错: {}".format(str(e)))
            finally:
                os.unlink(tmpfile_path)

        
        def highlight_line(self, line_number):
            # Line numbers start at 1, but Gtk.TextIter lines start at 0
            line_iter = self.textbuffer.get_iter_at_line(line_number - 1)
            self.textbuffer.place_cursor(line_iter)
            self.textview.scroll_to_iter(line_iter, 0.0, True, 0.0, 0.5)
        
        def update_status(self, message):
            self.statusbar.pop(self.context_id)
            self.statusbar.push(self.context_id, message)
        
        def show_info(self, title, message):
            dialog = Gtk.MessageDialog(
                parent=self.parent,
                modal= True,
                message_type=Gtk.MessageType.INFO,
                buttons=Gtk.ButtonsType.OK,
                text=message
            )
            dialog.set_title(title)
            dialog.run()
            dialog.destroy()
        
        def show_error(self, message):
            dialog = Gtk.MessageDialog(
                parent=self.parent,
                modal= True,
                message_type=Gtk.MessageType.ERROR,
                buttons=Gtk.ButtonsType.OK,
                text=message
            )
            dialog.set_title("Error")
            dialog.run()
            dialog.destroy()
        
        def show_output(self, title, message):
            dialog = Gtk.Dialog(title=title, parent=self.parent, flags=0)
            dialog.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK)
            dialog.set_default_size(500, 400)
            
            # Create scrolled window for output
            scrolled_window = Gtk.ScrolledWindow()
            scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
            dialog.get_content_area().pack_start(scrolled_window, True, True, 0)
            
            # Create text view for output
            textview = Gtk.TextView()
            textview.set_editable(False)
            textview.set_wrap_mode(Gtk.WrapMode.WORD)
            textbuffer = textview.get_buffer()
            textbuffer.set_text(message)
            scrolled_window.add(textview)
            
            dialog.show_all()
            dialog.run()
            dialog.destroy()

    # if __name__ == "__main__":
    #     win = Gtk.Window(title="Python Script Editor")
    #     win.set_default_size(800, 600)
    #     text = ScriptEditor(win)
    #     win.add(text)
    #     win.show_all()
        
    #     win.connect("destroy", Gtk.main_quit)
    #     Gtk.main()


    gi.require_version('Gtk', '3.0')

    @Singleton
    class OutputLogBox(Gtk.Frame):

        def __init__(self, parent):
            """创建输出区域"""
            super().__init__()
            self.parent = parent
            
            self.set_shadow_type(Gtk.ShadowType.IN)
            self.set_margin_top(5)
                    
            # 创建滚动窗口
            scrolled_window = Gtk.ScrolledWindow()
            scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
            self.add(scrolled_window)
            
            # 创建文本视图
            self.text_view = Gtk.TextView()
            self.text_view.set_editable(False)
            self.text_view.set_cursor_visible(False)
            self.text_view.set_wrap_mode(Gtk.WrapMode.WORD)
            scrolled_window.add(self.text_view)
            
            # 创建文本缓冲区
            self.text_buffer = self.text_view.get_buffer()
            
            # 创建标签表用于颜色
            self.tag_table = self.text_buffer.get_tag_table()
            
            # 创建颜色标签
            self.create_text_tags()
            
            # 添加初始内容
            self.output_lines = []
            self.add_output_line("系统初始化完成", "info")
            self.add_output_line("等待用户操作...", "info")
            
        
        def create_text_tags(self):
            """创建文本标签样式"""
            # 信息样式（默认）
            tag_info = Gtk.TextTag(name="info")
            tag_info.set_property("foreground", "#333333")
            self.tag_table.add(tag_info)
            
            # 成功样式
            tag_success = Gtk.TextTag(name="success")
            tag_success.set_property("foreground", "#008000")
            tag_success.set_property("weight", Pango.Weight.BOLD)
            self.tag_table.add(tag_success)
            
            # 警告样式
            tag_warning = Gtk.TextTag(name="warning")
            tag_warning.set_property("foreground", "#FF8C00")
            tag_warning.set_property("weight", Pango.Weight.BOLD)
            self.tag_table.add(tag_warning)
            
            # 错误样式
            tag_error = Gtk.TextTag(name="error")
            tag_error.set_property("foreground", "#FF0000")
            tag_error.set_property("weight", Pango.Weight.BOLD)
            self.tag_table.add(tag_error)
            
            # 标题样式
            tag_title = Gtk.TextTag(name="title")
            tag_title.set_property("size", 12 * Pango.SCALE)
            tag_title.set_property("weight", Pango.Weight.BOLD)
            tag_title.set_property("foreground", "#000080")
            self.tag_table.add(tag_title)
        
        
        def add_output_line(self, text, tag_name="info"):
            """添加输出行到文本视图"""
            timestamp = datetime.now().strftime("%H:%M:%S")
            line = "[{}] {}\n".format(timestamp,text)
            
            # 添加到内部存储
            self.output_lines.append((line, tag_name))
            
            # 更新文本缓冲区
            end_iter = self.text_buffer.get_end_iter()
            self.text_buffer.insert_with_tags_by_name(end_iter, line, tag_name)
            
            # 自动滚动到底部
            self.scroll_to_bottom()
        
        def scroll_to_bottom(self):
            """滚动到文本底部"""
            end_iter = self.text_buffer.get_end_iter()
            mark = self.text_buffer.create_mark(None, end_iter, False)
            self.text_view.scroll_to_mark(mark, 0.0, True, 0.0, 1.0)
            
        
        def on_export_log(self):
            """导出日志按钮点击事件"""
            if not self.output_lines:
                self.add_output_line("没有日志可导出", "warning")
                return 
                
            self.add_output_line("开始导出日志...", "info")   
                
            # 创建文件选择对话框
            dialog = Gtk.FileChooserDialog(
                title="保存日志",
                parent=self.parent.get_toplevel(),
                action=Gtk.FileChooserAction.SAVE,
                buttons=(
                    "取消", Gtk.ResponseType.CANCEL,
                    "保存", Gtk.ResponseType.OK
                )
            )
            
            # 设置默认文件名
            current_time = datetime.now().strftime("%Y%m%d_%H%M%S")
            dialog.set_current_name("operation_log_{}.log".format(current_time))
            
            # 添加文件过滤器
            filter_log = Gtk.FileFilter()
            filter_log.set_name("日志文件 (*.log)")
            filter_log.add_pattern("*.log")
            dialog.add_filter(filter_log)
            
            filter_all = Gtk.FileFilter()
            filter_all.set_name("所有文件")
            filter_all.add_pattern("*")
            dialog.add_filter(filter_all)
            
            # 显示对话框
            response = dialog.run()
            
            if response == Gtk.ResponseType.OK:
                filename = dialog.get_filename()
                if not filename.endswith(".log"):
                    filename += ".log"
                    
                # 模拟导出日志
                self.add_output_line("导出日志到: {}".format(filename), "info")
                
                try:
                    # 实际应用中这里会写入文件
                    with open(filename, "w") as f:
                        for line, _ in self.output_lines:
                            f.write(line)
                    os.chmod(filename,0o777)
                    
                    self.add_output_line("日志导出成功", "success")
                except Exception as e:
                    self.add_output_line("日志导出失败: {}".format(str(e)), "error")
            else:
                self.add_output_line("日志导出已取消", "warning")
            
            dialog.destroy()
            
            

    @Singleton
    class ErrorDialog:
        """错误提示对话框类"""
        
        def __init__(self, parent=None):
            self.parent = parent
            self.dialog = None
        
        def show_error(self, title, message, details=None, buttons=None):
            """
            显示错误对话框
            :param title: 对话框标题
            :param message: 主要错误信息
            :param details: 详细错误信息（可选）
            :param buttons: 自定义按钮（默认为"确定"）
            """
            # 创建对话框 - 修复弃用警告
            self.dialog = Gtk.MessageDialog(
                transient_for=self.parent.get_toplevel(),
                modal=True,
                destroy_with_parent=True,
                message_type=Gtk.MessageType.ERROR,
                buttons=Gtk.ButtonsType.NONE,  # 初始无按钮，后面添加自定义按钮
                text=title
            )
            
            # 设置对话框样式
            self.dialog.set_default_size(300, 200)
            self.dialog.set_border_width(10)
            self.dialog.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
            
            # 设置主消息
            self.dialog.format_secondary_markup("<b>{}</b>".format(message))
            
            # 添加自定义按钮
            if buttons:
                for button_text, response_id in buttons:
                    btn = self.dialog.add_button(button_text, response_id)
                    # 为复制按钮添加特殊样式
                    if button_text == "复制错误":
                        btn.get_style_context().add_class("suggested-action")
            
            # 添加详细错误信息区域
            if details:
                # 创建可滚动的文本视图
                scrolled_window = Gtk.ScrolledWindow()
                scrolled_window.set_policy(
                    Gtk.PolicyType.AUTOMATIC, 
                    Gtk.PolicyType.AUTOMATIC
                )
                scrolled_window.set_min_content_height(150)
                
                # 创建文本缓冲区
                text_view = Gtk.TextView()
                text_view.set_editable(False)
                text_view.set_cursor_visible(False)
                text_view.set_wrap_mode(Gtk.WrapMode.WORD)
                text_buffer = text_view.get_buffer()
                text_buffer.set_text(details)
                
                # 添加到滚动窗口
                scrolled_window.add(text_view)
                # 添加到对话框内容区域
                content_area = self.dialog.get_content_area()
                content_area.pack_end(scrolled_window, True, True, 10)
                
                # 显示所有组件
                content_area.show_all()
            
            # 连接响应信号
            self.dialog.connect("response", self.on_response)
            
            # 显示对话框
            self.dialog.show_all()
            
        
        def on_response(self, dialog, response_id):
            """处理对话框响应"""
            # 处理自定义按钮响应
            if response_id == 100:  # 复制错误
                self.on_copy_error(dialog)
            elif response_id == 102:  # 重试
                self.on_retry(dialog)
            else:
                if response_id == Gtk.ResponseType.CANCEL:
                    if hasattr(self.parent,'execute_btn'):
                        self.parent.execute_btn.set_sensitive(True)
                        self.parent._update_progress_idle(1, '重置异常状态', self.parent.execute_btn)
                # 标准按钮响应
                self.dialog.destroy()
                self.dialog = None
        
        
        def show_exception(self, exc_type, exc_value, exc_traceback):
            """显示异常的错误对话框"""
            # exc_type, exc_value, exc_traceback = sys.exc_info()
            error_title = "错误: {}".format(exc_type.__name__)
            error_message = str(exc_value) or "发生未知错误"
            
            # 获取详细堆栈信息
            error_details = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))
            
            # 添加自定义按钮
            buttons = [
                ("复制错误", 100),
                ("忽略", Gtk.ResponseType.CANCEL),
                ("退出", Gtk.ResponseType.CLOSE),
                # ("确定", Gtk.ResponseType.OK)
            ]
            
            # 显示错误对话框
            self.show_error(
                title=error_title,
                message=error_message,
                details=error_details,
                buttons=buttons
            )
        
        def on_copy_error(self, widget):
            """复制错误信息到剪贴板"""
            content_area = self.dialog.get_content_area()
            for child in content_area.get_children():
                if isinstance(child, Gtk.ScrolledWindow):
                    text_view = child.get_child()
                    buffer = text_view.get_buffer()
                    start = buffer.get_start_iter()
                    end = buffer.get_end_iter()
                    text = buffer.get_text(start, end, False)
                    
                    # 获取剪贴板
                    clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
                    clipboard.set_text(text, -1)
                    return
        
        
        def on_retry(self, widget):
            """重试回调"""
            self.dialog.destroy()
            self.dialog = None


    # if __name__ == "__main__":
    #     win = Gtk.Window(title="Python Script Editor")
    #     win.set_default_size(800, 600)
    #     text = OutputLogBox(win).on_export_log()
    #     # win.add(text)
    #     win.show_all()
        
    #     win.connect("destroy", Gtk.main_quit)
    #     Gtk.main()




class ColorPrinter:
    """
    彩色文字打印工具类
    
    支持的颜色:
        black, red, green, yellow, blue, magenta, cyan, white, light_black,
        light_red, light_green, light_yellow, light_blue, light_magenta, 
        light_cyan, light_white
        
    支持的样式:
        bold, dim, italic, underline, blink, reverse, hidden, strike
        
    使用方法:
        print_color(text, color, style)
        print_background(text, bg_color)
        print_with_style(text, color, bg_color, style)
    """
    
    # ANSI 转义代码
    COLORS = {
        'black': 30,
        'red': 31,
        'green': 32,
        'yellow': 33,
        'blue': 34,
        'magenta': 35,
        'cyan': 36,
        'white': 37,
        'light_black': 90,
        'light_red': 91,
        'light_green': 92,
        'light_yellow': 93,
        'light_blue': 94,
        'light_magenta': 95,
        'light_cyan': 96,
        'light_white': 97,
    }
    
    BACKGROUNDS = {
        'black': 40,
        'red': 41,
        'green': 42,
        'yellow': 43,
        'blue': 44,
        'magenta': 45,
        'cyan': 46,
        'white': 47,
        'light_black': 100,
        'light_red': 101,
        'light_green': 102,
        'light_yellow': 103,
        'light_blue': 104,
        'light_magenta': 105,
        'light_cyan': 106,
        'light_white': 107,
    }
    
    STYLES = {
        'bold': 1,
        'dim': 2,
        'italic': 3,
        'underline': 4,
        'blink': 5,
        'reverse': 7,
        'hidden': 8,
        'strike': 9,
    }
    
    RESET = '\033[0m'

    @classmethod
    def print_color(cls, text: str, color: str = None) -> None:
        """
        打印彩色文字
        
        :param text: 要打印的文字
        :param color: 颜色名称，如 'red', 'blue'
        """
        if color and color in cls.COLORS:
            code = cls.COLORS[color]
            print("\033[{}m{}{}".format(code,text,cls.RESET))
        else:
            print(text)

    @classmethod
    def print_background(cls, text: str, bg_color: str = None) -> None:
        """
        打印带背景色的文字
        
        :param text: 要打印的文字
        :param bg_color: 背景颜色名称
        """
        if bg_color and bg_color in cls.BACKGROUNDS:
            code = cls.BACKGROUNDS[bg_color]
            print("\033[{}m{}{}".format(code,text,cls.RESET))
        else:
            print(text)

    @classmethod
    def print_with_style(cls, text: str, color: str = None, 
                         bg_color: str = None, style: str = None) -> None:
        """
        打印带颜色、背景和样式的文字
        
        :param text: 要打印的文字
        :param color: 文字颜色
        :param bg_color: 背景颜色
        :param style: 文字样式
        """
        codes = []
        
        if color and color in cls.COLORS:
            codes.append(str(cls.COLORS[color]))
        
        if bg_color and bg_color in cls.BACKGROUNDS:
            codes.append(str(cls.BACKGROUNDS[bg_color]))
        
        if style and style in cls.STYLES:
            codes.append(str(cls.STYLES[style]))
        
        if codes:
            code_str = ';'.join(codes)
            print("\033[{}m{}{}".format(code_str,text,cls.RESET))
        else:
            print(text)


@Singleton
class SecClient(ColorPrinter):
    
    def __init__(self, categories):
        self.categories = categories
        # 当前配置
        self.system_config = {} 
        self.logger = logging.getLogger(__name__)
        
         
    def load_config(self,filename):
        try:
            with open(filename ,'r') as f:
                self.all_config = json.load(f)
                self.print_color("导入配置成功 {}".format(filename),"green")
        except Exception as e:
            self.print_color("导入配置失败 {} ".format(filename),"red")
    
    
    def restore_config(self,backup_file:str):
        # 执行还原
        if backup_file.startswith('.') or backup_file.startswith('/'):
            backup_path = backup_file
        else:
            backup_path = os.path.join("/var/log/sectool/",backup_file)
        if not os.path.exists(backup_path):
            self.print_color('要恢复的文件 {} 不存在'.format(backup_path), 'red')
            return False
        backup_infos = {}
        with open(backup_path, 'r') as f:
            backup_infos = json.load(f)
        for class_name,conf in backup_infos.items():
            for category in self.categories.values():
                if category.__class__.__name__ == class_name:
                    self.print_color('正在还原 {}'.format(class_name),'cyan')
                    category.restore(conf)
        self.print_color("还原任务执行完成\n", "green")
        return True

    
    def backup_config(self):
        backup_infos = {}
        suffix = datetime.now().strftime("%Y%m%d%H%M%S")
        for category in  self.categories.values():
            info = category.backup(suffix)
            backup_infos.update( info )
        backup_file = os.path.join("/var/log/sectool/", "{}.bak".format(suffix) )
        with open(backup_file, 'w') as f:
            json.dump(backup_infos,f,indent=4)
        self.print_color('当前配置备份成功 {}'.format(backup_file),'green')
    
    
    def list_backups(self):
        # 添加还原点到列表
        backup_files = glob.glob('/var/log/sectool/*.bak') 
        self.print_color('下面是位于 /var/log/sectool/ 备份, 共有 {} 个：'.format(len(backup_files)))
        for file in backup_files:
            file_name = os.path.basename(file)
            self.print_color(file_name,'blue')

    
    def read_config(self):
        for key,category in self.categories.items():
            print('正在获取配置 {}'.format(key))
            self.system_config[key] =  category.read_config()
        

    def write_config(self, filename):
        if not os.path.exists(filename):
            self.print_color('{} 文件不存在'.format(filename),'red')
            return False
        try:
            self.load_config(filename)
            self.backup_config()
            for key,config in self.all_config.items():
                self.logger.debug('{} => {}'.format(key,config))
                self.categories[key].write_config(config)
                self.print_color("配置写入完成 {} ".format(key),"green")
            self.print_color("所有任务执行完成\n", "green")
        except Exception as e:
            raise

    
# if __name__ == '__main__':
#     SecClient([]).list_backups()

    # from actions.auth import AuthManager
    # from actions.user import UserManager
    # from actions.service import ServiceManager
    # from actions.ssh import SSHManager
    # from actions.iptables import IPTablesManager
    # from actions.audit import AuditLogManager
    # from actions.other import Other
    # logging.basicConfig(
    #     level=logging.INFO,
    #     format="%(asctime)s - [%(filename)s:%(lineno)d] - %(levelname)s - %(message)s",
    #     datefmt="%Y-%m-%d %H:%M:%S"
    # )
    
    # categories = {'auth': AuthManager(), 
    #               'user': UserManager(), 
    #               'service': ServiceManager(), 
    #               'ssh': SSHManager(), 
    #               'iptables': IPTablesManager(),
    #               'audit':AuditLogManager(), 
    #               'other':Other() 
    #             }
    # sec = SecClient(categories)
    # print(sys.argv)
    # if len(sys.argv) > 1 :
    #     sec.write_config(sys.argv[1])
    # else:
    #     print('参数不够')




def parse_cmdline():
    # 创建主解析器
    parser = argparse.ArgumentParser(
        prog="sectool.py",
        description="一个系统加固工具",
        epilog="请谨慎使用该工具，如何对系统有影响，可以一键还原!",
        add_help=False
    )
    # 添加全局参数
    # 添加自定义帮助选项
    parser.add_argument(
        '-h', '--help', 
        action='help',
        default=argparse.SUPPRESS,
        help='\033[32m显示此帮助信息并退出\033[0m'
    )
    parser.add_argument("--version",  action="version", version="%(prog)s 1.0.0", help='显示版本')
    # parser.add_argument("-v", "--verbose",  action="store_true",  help="启用详细输出模式")
    # parser.add_argument('-u','--ui', default='gui', choices=["gui", "cmdline"], help="指定软件的运行方式")
    group = parser.add_mutually_exclusive_group()
    group.add_argument('-f', '--configfile', type=str, help="指定要导入的文件")
    group.add_argument('-r', '--restorefile', type=str, help="指定要恢复的文件")
    group.add_argument('-l', '--list-backups', action='store_true', help="列出所有备份")

    if not os.environ.get('DISPLAY'):
        parser.print_help()
        
    # 解析命令行参数
    args = parser.parse_args()
    return args

# 需要获取当前用户的环境变量： 
#   sudo -E DISPLAY=$DISPLAY DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS $0
#   sudo -E XAUTHORITY=$XAUTHORITY  python3 $0
if __name__ == "__main__":

    if os.getuid() != 0:  
        script_path = os.path.abspath(__file__)
        # 使用 sudo -E 保留环境变量并重启脚本
        subprocess.call(["sudo", "-E", "DBUS_SESSION_BUS_ADDRESS={}".format(os.environ.get('DBUS_SESSION_BUS_ADDRESS')), sys.executable, script_path, *sys.argv[1:]])
        sys.exit(0)  #
    else:
        # 运行应用
        logging.basicConfig(
                filename=LOG_FILE,       # 日志文件名
                filemode='w',            # 模式：'a' 追加，'w' 覆盖
                level=logging.DEBUG,
                format="%(asctime)s - [%(filename)s:%(lineno)d] - %(levelname)s - %(message)s",
                datefmt="%Y-%m-%d %H:%M:%S"
            )
        categories = OrderedDict([
                    ( 'auth',AuthManager() ), 
                    ( 'user', UserManager() ), 
                    ( 'service', ServiceManager() ),  
                    ( 'ssh', SSHManager() ), 
                    ( 'iptables', IPTablesManager() ), 
                    ( 'audit', AuditLogManager() ), 
                    ( 'other', Other() ), 
                ])
        
        cmd_params = parse_cmdline()

        config_file = cmd_params.configfile
        restore_file = cmd_params.restorefile
        if config_file:
            cmd_client = SecClient(categories)
            cmd_client.write_config(config_file)
        elif restore_file:
            cmd_client = SecClient(categories)
            cmd_client.restore_config(restore_file)
        elif cmd_params.list_backups:
            cmd_client = SecClient(categories)
            cmd_client.list_backups()
        elif os.environ.get('DISPLAY'):
            win = MainWindow(list(categories.values()))
            win.run()