From 97cc2bb0f7dd6d310120ae5058a88778770dd892 Mon Sep 17 00:00:00 2001
From: xiaoli-cli
-
+
@@ -35,6 +35,7 @@
- **定时任务** — 支持相对时间、绝对时间、重复任务,到期自动激活 AI
- **系统通知** — 跨平台任务完成通知(Windows / macOS / Linux)
- **MCP 协议** — 19 个插件全部实现 MCP 定义,支持 Function Calling 原生调用
+- **进程保护** — 跨平台单实例保护 + 进程优先级提升 + Windows PPL 风格保护 + 看门狗监控 + 优雅关闭
- **19 个插件 161+ 个操作** — 代码编辑、Git、浏览器、工程化、文件管理、图像生成、记忆等
- **三模式界面** — TUI 图形化 + 传统命令行 + Web UI 浏览器界面
- **150+ 个自动化测试** — pytest 覆盖全部插件和核心模块
diff --git a/platform_utils.py b/platform_utils.py
index a019876..1bf792a 100644
--- a/platform_utils.py
+++ b/platform_utils.py
@@ -121,4 +121,79 @@ def get_config_directory():
elif platform == 'macos':
return os.path.join(os.path.expanduser("~"), 'Library', 'Application Support', 'Xiaoli Pro-CLI 3')
else: # Linux
- return os.path.join(os.path.expanduser('~'), '.config', 'xiaoli-pro-cli-3')
\ No newline at end of file
+ return os.path.join(os.path.expanduser('~'), '.config', 'xiaoli-pro-cli-3')
+
+def is_process_running(pid):
+ """检查指定 PID 的进程是否在运行"""
+ platform = get_platform()
+
+ try:
+ if platform == 'windows':
+ import ctypes
+ kernel32 = ctypes.windll.kernel32
+ handle = kernel32.OpenProcess(0x1000, False, pid) # PROCESS_QUERY_LIMITED_INFORMATION
+ if handle:
+ kernel32.CloseHandle(handle)
+ return True
+ return False
+ else:
+ os.kill(pid, 0)
+ return True
+ except (OSError, ProcessLookupError):
+ return False
+
+
+def get_process_priority():
+ """获取当前进程优先级"""
+ platform = get_platform()
+
+ try:
+ if platform == 'windows':
+ import ctypes
+ kernel32 = ctypes.windll.kernel32
+ handle = kernel32.GetCurrentProcess()
+ return kernel32.GetPriorityClass(handle)
+ else:
+ import resource
+ return resource.getpriority(resource.PRIO_PROCESS, 0)
+ except Exception:
+ return None
+
+
+def set_process_priority(level="above_normal"):
+ """
+ 设置进程优先级
+
+ Args:
+ level: "below_normal" | "normal" | "above_normal" | "high"
+
+ Returns:
+ 是否设置成功
+ """
+ platform = get_platform()
+
+ try:
+ if platform == 'windows':
+ import ctypes
+ kernel32 = ctypes.windll.kernel32
+ priority_map = {
+ "below_normal": 0x00004000,
+ "normal": 0x00000020,
+ "above_normal": 0x00008000,
+ "high": 0x00000080,
+ }
+ priority = priority_map.get(level, 0x00008000)
+ handle = kernel32.GetCurrentProcess()
+ return bool(kernel32.SetPriorityClass(handle, priority))
+ else:
+ nice_map = {
+ "below_normal": 10,
+ "normal": 0,
+ "above_normal": -10,
+ "high": -15,
+ }
+ nice_val = nice_map.get(level, -10)
+ os.nice(nice_val)
+ return True
+ except Exception:
+ return False
diff --git a/xcli_core/cli_base.py b/xcli_core/cli_base.py
index 31f551f..6ad3af1 100644
--- a/xcli_core/cli_base.py
+++ b/xcli_core/cli_base.py
@@ -11,6 +11,7 @@ from .constants import (
)
from .config import get_system_config, set_system_config, logger
from .plugin_manager import LiuginManager
+from .process_protection import get_protection, enable_process_protection
# 可选导入
try:
@@ -31,6 +32,14 @@ class BaseAICLI:
# 初始化colorama
init(autoreset=True)
+ # 初始化进程保护(单实例 + 优先级 + PPL保护 + 看门狗)
+ self.protection_results = enable_process_protection(
+ priority_level="above_normal",
+ watchdog=True,
+ restart_on_crash=False,
+ )
+ self._protection = get_protection()
+
# 确保插件管理器使用正确的路径
project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
liugins_dir = os.path.join(project_dir, "plugins")
@@ -114,6 +123,12 @@ class BaseAICLI:
def cleanup_resources(self):
"""清理资源,确保在程序退出时正确关闭所有资源"""
print(f"{Fore.YELLOW}正在清理资源...{Style.RESET_ALL}")
+ # 清理进程保护
+ if hasattr(self, '_protection') and self._protection:
+ try:
+ self._protection.disable_all()
+ except Exception:
+ pass
self._stop_clawli_monitor()
if CLAWLI_SERVER_AVAILABLE and clawli_server:
clawli_server.stop_server()
diff --git a/xcli_core/cli_core.py b/xcli_core/cli_core.py
index 6a505fc..84d5cd6 100644
--- a/xcli_core/cli_core.py
+++ b/xcli_core/cli_core.py
@@ -20,6 +20,7 @@ from .cli_display import DisplayMixin
from .cli_history import HistoryMixin
from .plugin_market import PluginMarketMixin
from .notification import notify_task_complete, get_notification_manager
+from .process_protection import get_protection_status
class AICLI(BaseAICLI, ClawliMixin, ToolMixin, CodeExecMixin, DisplayMixin, HistoryMixin, PluginMarketMixin):
@@ -1020,6 +1021,7 @@ multi 操作支持一次修改多处:
print(f"{Fore.GREEN}输入 '/chat open <名称>' 加载聊天记录{Style.RESET_ALL}")
print(f"{Fore.GREEN}输入 '/remote' 查看远程连接帮助{Style.RESET_ALL}")
print(f"{Fore.GREEN}输入 '/plugin' 管理插件市场 (安装/卸载/搜索){Style.RESET_ALL}")
+ print(f"{Fore.GREEN}输入 '/protect' 查看进程保护状态{Style.RESET_ALL}")
current_engine_name = getattr(self.current_engine, 'name', '未设置') if self.current_engine else '未设置'
print(f"{Fore.GREEN}当前使用的AI引擎: {current_engine_name}{Style.RESET_ALL}")
loaded_engines = list(self.engines.keys())
@@ -1027,6 +1029,15 @@ multi 操作支持一次修改多处:
print(f"{Fore.GREEN}已加载的AI引擎: {', '.join(loaded_engines)}{Style.RESET_ALL}")
print(f"{Fore.GREEN}{'-' * 50}{Style.RESET_ALL}")
+ # 显示进程保护状态
+ if hasattr(self, 'protection_results') and self.protection_results:
+ r = self.protection_results
+ si_icon = '' if r.get('single_instance') else ''
+ prio_icon = '' if r.get('priority') else ''
+ ppl_icon = '' if r.get('ppl_protection') else ''
+ wd_icon = '' if r.get('watchdog') else ''
+ print(f"{Fore.CYAN} 进程保护: {si_icon}单实例 {prio_icon}优先级 {ppl_icon}PPL保护 {wd_icon}看门狗{Style.RESET_ALL}")
+
# 询问 diff 弹窗模式
self._ask_diff_popup_mode()
@@ -1171,6 +1182,29 @@ multi 操作支持一次修改多处:
print(result)
continue
+ if user_input.startswith('/protect'):
+ parts = user_input.split()
+ if len(parts) > 1 and parts[1] == 'test':
+ # 重新测试所有保护
+ from .process_protection import enable_process_protection
+ results = enable_process_protection()
+ print(f"\n{Fore.CYAN} 进程保护测试结果:{Style.RESET_ALL}")
+ for key, val in results.items():
+ icon = '' if val else ''
+ print(f" {icon} {key}: {val}")
+ else:
+ status = get_protection_status()
+ print(f"\n{Fore.CYAN} 进程保护状态:{Style.RESET_ALL}")
+ print(f" 平台: {status.get('platform', '未知')}")
+ icon = '' if status.get('is_protected') else ''
+ print(f" {icon} 保护已启用: {status.get('is_protected', False)}")
+ icon = '' if status.get('single_instance_held') else ''
+ print(f" {icon} 单实例锁: {status.get('single_instance_held', False)}")
+ icon = '' if status.get('watchdog_running') else ''
+ print(f" {icon} 看门狗: {status.get('watchdog_running', False)}")
+ print(f" 关闭钩子: {status.get('shutdown_hooks_count', 0)} 个")
+ continue
+
if user_input.startswith('/remote'):
remote_command = user_input[7:].strip()
self._handle_remote_command(remote_command)
diff --git a/xcli_core/process_protection.py b/xcli_core/process_protection.py
new file mode 100644
index 0000000..fde7b3f
--- /dev/null
+++ b/xcli_core/process_protection.py
@@ -0,0 +1,659 @@
+"""
+跨平台进程保护模块
+提供单实例保护、进程优先级提升、看门狗监控、优雅关闭等功能
+
+支持 Windows / macOS / Linux
+Windows 特有: PPL (Protected Process Light) 风格保护、错误模式抑制
+"""
+import os
+import sys
+import time
+import signal
+import atexit
+import threading
+import subprocess
+from typing import Optional, Callable
+
+
+# ── 平台检测 ──
+PLATFORM = (
+ 'windows' if os.name == 'nt'
+ else 'macos' if sys.platform == 'darwin'
+ else 'linux'
+)
+
+
+class ProcessProtection:
+ """跨平台进程保护器"""
+
+ def __init__(self, app_name: str = "xiaoli-cli"):
+ self.app_name = app_name
+ self._lock_file_path: Optional[str] = None
+ self._lock_fd = None
+ self._watchdog_thread: Optional[threading.Thread] = None
+ self._watchdog_running = threading.Event()
+ self._shutdown_hooks: list[Callable] = []
+ self._is_protected = False
+ self._original_priority = None
+ self._restart_on_crash = False
+ self._restart_script: Optional[str] = None
+
+ # ══════════════════════════════════════════════════
+ # 单实例保护
+ # ══════════════════════════════════════════════════
+
+ def acquire_single_instance(self) -> bool:
+ """
+ 获取单实例锁,防止重复启动
+ 返回 True 表示成功(当前是唯一实例),False 表示已有实例在运行
+ """
+ if PLATFORM == 'windows':
+ return self._acquire_windows_mutex()
+ else:
+ return self._acquire_posix_lock()
+
+ def release_single_instance(self):
+ """释放单实例锁"""
+ if PLATFORM == 'windows':
+ self._release_windows_mutex()
+ else:
+ self._release_posix_lock()
+
+ def _acquire_windows_mutex(self) -> bool:
+ """Windows: 使用命名 Mutex 实现单实例"""
+ try:
+ import ctypes
+ from ctypes import wintypes
+
+ kernel32 = ctypes.windll.kernel32
+ mutex_name = f"Global\\{self.app_name}_single_instance"
+
+ # 创建命名 Mutex
+ handle = kernel32.CreateMutexW(None, True, mutex_name)
+ last_error = kernel32.GetLastError()
+
+ ERROR_ALREADY_EXISTS = 183
+ if last_error == ERROR_ALREADY_EXISTS:
+ kernel32.CloseHandle(handle)
+ return False
+
+ self._win_mutex_handle = handle
+ atexit.register(self.release_single_instance)
+ return True
+
+ except Exception:
+ # 降级到文件锁
+ return self._acquire_posix_lock()
+
+ def _release_windows_mutex(self):
+ """Windows: 释放 Mutex"""
+ try:
+ if hasattr(self, '_win_mutex_handle') and self._win_mutex_handle:
+ import ctypes
+ ctypes.windll.kernel32.CloseHandle(self._win_mutex_handle)
+ self._win_mutex_handle = None
+ except Exception:
+ pass
+
+ def _acquire_posix_lock(self) -> bool:
+ """POSIX (macOS/Linux): 使用文件锁实现单实例"""
+ import tempfile
+
+ lock_dir = tempfile.gettempdir()
+ self._lock_file_path = os.path.join(lock_dir, f"{self.app_name}.lock")
+
+ try:
+ # 尝试创建并锁定文件
+ if sys.platform == 'darwin':
+ # macOS: 使用 fcntl 文件锁
+ import fcntl
+ self._lock_fd = open(self._lock_file_path, 'w')
+ try:
+ fcntl.flock(self._lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
+ except (IOError, OSError):
+ # 文件已被锁定 — 已有实例在运行
+ # 检查锁文件中的 PID 是否仍在运行
+ try:
+ old_pid = self._lock_fd.read().strip()
+ if old_pid and self._is_process_running(int(old_pid)):
+ self._lock_fd.close()
+ self._lock_fd = None
+ return False
+ else:
+ # 旧进程已死,清理残留锁
+ self._lock_fd.seek(0)
+ self._lock_fd.truncate()
+ except (ValueError, OSError):
+ pass
+
+ self._lock_fd.seek(0)
+ self._lock_fd.truncate()
+ self._lock_fd.write(str(os.getpid()))
+ self._lock_fd.flush()
+ atexit.register(self.release_single_instance)
+ return True
+ else:
+ # Linux: 使用 fcntl 文件锁
+ import fcntl
+ self._lock_fd = open(self._lock_file_path, 'w')
+ try:
+ fcntl.flock(self._lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
+ except (IOError, OSError):
+ # 检查旧进程是否仍在运行
+ try:
+ old_pid = self._lock_fd.read().strip()
+ if old_pid and self._is_process_running(int(old_pid)):
+ self._lock_fd.close()
+ self._lock_fd = None
+ return False
+ else:
+ self._lock_fd.seek(0)
+ self._lock_fd.truncate()
+ except (ValueError, OSError):
+ pass
+
+ self._lock_fd.seek(0)
+ self._lock_fd.truncate()
+ self._lock_fd.write(str(os.getpid()))
+ self._lock_fd.flush()
+ atexit.register(self.release_single_instance)
+ return True
+
+ except Exception:
+ # 锁获取失败,但不阻止程序运行
+ return True
+
+ def _release_posix_lock(self):
+ """POSIX: 释放文件锁"""
+ try:
+ if self._lock_fd:
+ if sys.platform == 'darwin':
+ import fcntl
+ fcntl.flock(self._lock_fd.fileno(), fcntl.LOCK_UN)
+ else:
+ import fcntl
+ fcntl.flock(self._lock_fd.fileno(), fcntl.LOCK_UN)
+ self._lock_fd.close()
+ self._lock_fd = None
+ if self._lock_file_path and os.path.exists(self._lock_file_path):
+ try:
+ os.unlink(self._lock_file_path)
+ except OSError:
+ pass
+ except Exception:
+ pass
+
+ @staticmethod
+ def _is_process_running(pid: int) -> bool:
+ """检查指定 PID 的进程是否在运行"""
+ try:
+ if PLATFORM == 'windows':
+ import ctypes
+ kernel32 = ctypes.windll.kernel32
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
+ handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
+ if handle:
+ kernel32.CloseHandle(handle)
+ return True
+ return False
+ else:
+ os.kill(pid, 0)
+ return True
+ except (OSError, ProcessLookupError):
+ return False
+
+ # ══════════════════════════════════════════════════
+ # 进程优先级提升
+ # ══════════════════════════════════════════════════
+
+ def elevate_priority(self, level: str = "high") -> bool:
+ """
+ 提升进程优先级
+
+ Args:
+ level: "above_normal" | "high" | "realtime"(仅Windows)
+
+ Returns:
+ 是否成功提升
+ """
+ try:
+ if PLATFORM == 'windows':
+ return self._elevate_windows(level)
+ elif PLATFORM == 'macos':
+ return self._elevate_macos(level)
+ else:
+ return self._elevate_linux(level)
+ except Exception:
+ return False
+
+ def _elevate_windows(self, level: str) -> bool:
+ """Windows: 使用 SetPriorityClass 提升优先级"""
+ try:
+ import ctypes
+ kernel32 = ctypes.windll.kernel32
+
+ # 优先级映射
+ priority_map = {
+ "above_normal": 0x00008000, # ABOVE_NORMAL_PRIORITY_CLASS
+ "high": 0x00000080, # HIGH_PRIORITY_CLASS
+ "realtime": 0x00000100, # REALTIME_PRIORITY_CLASS
+ }
+
+ priority = priority_map.get(level, 0x00000080)
+
+ # 获取当前进程句柄
+ handle = kernel32.GetCurrentProcess()
+
+ # 记录原始优先级
+ self._original_priority = kernel32.GetPriorityClass(handle)
+
+ # 设置新优先级
+ result = kernel32.SetPriorityClass(handle, priority)
+ if result:
+ self._is_protected = True
+ return True
+ return False
+
+ except Exception:
+ return False
+
+ def _elevate_macos(self, level: str) -> bool:
+ """macOS: 使用 renice 提升优先级"""
+ try:
+ nice_map = {
+ "above_normal": -10,
+ "high": -15,
+ "realtime": -20,
+ }
+ nice_val = nice_map.get(level, -10)
+ os.nice(nice_val)
+ self._is_protected = True
+ return True
+ except Exception:
+ return False
+
+ def _elevate_linux(self, level: str) -> bool:
+ """Linux: 使用 renice 提升优先级"""
+ try:
+ nice_map = {
+ "above_normal": -10,
+ "high": -15,
+ "realtime": -20,
+ }
+ nice_val = nice_map.get(level, -10)
+
+ # os.nice() 是相对值,需要先获取当前 nice 值
+ try:
+ import resource
+ current = resource.getpriority(resource.PRIO_PROCESS, 0)
+ target = nice_val
+ delta = target - current
+ if delta != 0:
+ os.nice(delta)
+ except (ImportError, OSError):
+ # fallback: 直接使用 subprocess
+ subprocess.run(
+ ["renice", str(nice_val), "-p", str(os.getpid())],
+ capture_output=True, timeout=5
+ )
+
+ self._is_protected = True
+ return True
+ except Exception:
+ return False
+
+ def restore_priority(self):
+ """恢复原始进程优先级"""
+ if not self._is_protected:
+ return
+
+ try:
+ if PLATFORM == 'windows' and self._original_priority:
+ import ctypes
+ kernel32 = ctypes.windll.kernel32
+ handle = kernel32.GetCurrentProcess()
+ kernel32.SetPriorityClass(handle, self._original_priority)
+ elif PLATFORM in ('macos', 'linux'):
+ # 恢复到默认 nice 值 (0)
+ try:
+ import resource
+ current = resource.getpriority(resource.PRIO_PROCESS, 0)
+ os.nice(-current)
+ except (ImportError, OSError):
+ subprocess.run(
+ ["renice", "0", "-p", str(os.getpid())],
+ capture_output=True, timeout=5
+ )
+ self._is_protected = False
+ except Exception:
+ pass
+
+ # ══════════════════════════════════════════════════
+ # Windows PPL 风格保护
+ # ══════════════════════════════════════════════════
+
+ def enable_ppl_protection(self) -> bool:
+ """
+ Windows: 启用 PPL (Protected Process Light) 风格保护
+ - 抑制错误弹窗
+ - 设置进程缓解策略
+ - 禁用 DEP 异常弹窗
+
+ macOS/Linux: 启用等效保护
+ - 设置信号处理
+ - 忽略 SIGHUP
+ """
+ if PLATFORM == 'windows':
+ return self._enable_windows_ppl()
+ else:
+ return self._enable_posix_protection()
+
+ def _enable_windows_ppl(self) -> bool:
+ """Windows: PPL 风格保护"""
+ try:
+ import ctypes
+ kernel32 = ctypes.windll.kernel32
+
+ # 1. 抑制系统错误弹窗 (SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX)
+ SEM_FAILCRITICALERRORS = 0x0001
+ SEM_NOGPFAULTERRORBOX = 0x0002
+ SEM_NOOPENFILEERRORBOX = 0x8000
+ old_mode = kernel32.SetErrorMode(
+ SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX
+ )
+ self._original_error_mode = old_mode
+
+ # 2. 设置进程为后台优先级模式(减少对用户交互的影响)
+ # PROCESS_MODE_BACKGROUND_BEGIN = 0x00100000
+ # kernel32.SetPriorityClass(kernel32.GetCurrentProcess(), 0x00100000)
+
+ # 3. 注册异常处理(防止未处理异常导致崩溃弹窗)
+ try:
+ import faulthandler
+ faulthandler.enable()
+ except Exception:
+ pass
+
+ self._is_protected = True
+ return True
+
+ except Exception:
+ return False
+
+ def _enable_posix_protection(self) -> bool:
+ """macOS/Linux: 等效保护"""
+ try:
+ # 1. 忽略 SIGHUP(终端关闭时不停止进程)
+ signal.signal(signal.SIGHUP, signal.SIG_IGN)
+
+ # 2. 注册优雅关闭信号
+ def _graceful_shutdown(signum, frame):
+ self._execute_shutdown_hooks()
+ sys.exit(0)
+
+ signal.signal(signal.SIGTERM, _graceful_shutdown)
+
+ # 3. macOS 特有:忽略 SIGPIPE
+ if PLATFORM == 'macos':
+ signal.signal(signal.SIGPIPE, signal.SIG_IGN)
+
+ # 4. 启用 faulthandler(崩溃时输出堆栈)
+ try:
+ import faulthandler
+ faulthandler.enable()
+ except Exception:
+ pass
+
+ self._is_protected = True
+ return True
+
+ except Exception:
+ return False
+
+ # ══════════════════════════════════════════════════
+ # 看门狗监控
+ # ══════════════════════════════════════════════════
+
+ def start_watchdog(self, check_interval: float = 5.0,
+ restart_on_crash: bool = False,
+ restart_script: Optional[str] = None):
+ """
+ 启动看门狗线程,监控父进程状态
+ 当父进程退出时,自动清理并退出
+
+ Args:
+ check_interval: 检查间隔(秒)
+ restart_on_crash: 崩溃时是否自动重启
+ restart_script: 重启脚本路径
+ """
+ self._restart_on_crash = restart_on_crash
+ self._restart_script = restart_script
+
+ if self._watchdog_thread and self._watchdog_thread.is_alive():
+ return
+
+ self._watchdog_running.set()
+ self._watchdog_thread = threading.Thread(
+ target=self._watchdog_loop,
+ args=(check_interval,),
+ daemon=True,
+ name="process-watchdog"
+ )
+ self._watchdog_thread.start()
+
+ def stop_watchdog(self):
+ """停止看门狗"""
+ self._watchdog_running.clear()
+ if self._watchdog_thread:
+ self._watchdog_thread.join(timeout=3)
+ self._watchdog_thread = None
+
+ def _watchdog_loop(self, check_interval: float):
+ """看门狗主循环"""
+ parent_pid = os.getppid()
+
+ while self._watchdog_running.is_set():
+ try:
+ # 检查父进程是否仍在运行
+ if not self._is_process_running(parent_pid):
+ # 父进程已退出,执行清理
+ self._execute_shutdown_hooks()
+ if self._restart_on_crash and self._restart_script:
+ self._do_restart()
+ else:
+ os._exit(0)
+ break
+
+ time.sleep(check_interval)
+
+ except Exception:
+ time.sleep(check_interval)
+
+ def _do_restart(self):
+ """执行重启"""
+ try:
+ if self._restart_script and os.path.exists(self._restart_script):
+ if PLATFORM == 'windows':
+ subprocess.Popen(
+ ["python", self._restart_script],
+ creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
+ )
+ else:
+ subprocess.Popen(
+ ["python3", self._restart_script],
+ start_new_session=True
+ )
+ except Exception:
+ pass
+ finally:
+ os._exit(0)
+
+ # ══════════════════════════════════════════════════
+ # 关闭钩子
+ # ══════════════════════════════════════════════════
+
+ def register_shutdown_hook(self, hook: Callable):
+ """注册关闭钩子,在进程退出时执行"""
+ if hook not in self._shutdown_hooks:
+ self._shutdown_hooks.append(hook)
+
+ def _execute_shutdown_hooks(self):
+ """执行所有关闭钩子"""
+ for hook in reversed(self._shutdown_hooks):
+ try:
+ hook()
+ except Exception:
+ pass
+
+ # ══════════════════════════════════════════════════
+ # 一键启用全部保护
+ # ══════════════════════════════════════════════════
+
+ def enable_all(self, priority_level: str = "above_normal",
+ watchdog: bool = True,
+ restart_on_crash: bool = False,
+ restart_script: Optional[str] = None) -> dict:
+ """
+ 一键启用所有进程保护
+
+ Args:
+ priority_level: 优先级 ("above_normal" | "high")
+ watchdog: 是否启用看门狗
+ restart_on_crash: 崩溃时是否自动重启
+ restart_script: 重启脚本路径
+
+ Returns:
+ 各项保护的启用状态字典
+ """
+ results = {
+ "single_instance": False,
+ "priority": False,
+ "ppl_protection": False,
+ "watchdog": False,
+ "platform": PLATFORM,
+ }
+
+ # 1. 单实例保护
+ results["single_instance"] = self.acquire_single_instance()
+
+ # 2. 进程优先级提升
+ results["priority"] = self.elevate_priority(priority_level)
+
+ # 3. PPL 风格保护
+ results["ppl_protection"] = self.enable_ppl_protection()
+
+ # 4. 看门狗
+ if watchdog:
+ self.start_watchdog(
+ restart_on_crash=restart_on_crash,
+ restart_script=restart_script
+ )
+ results["watchdog"] = True
+
+ # 注册清理
+ atexit.register(self.disable_all)
+
+ return results
+
+ def disable_all(self):
+ """禁用所有保护并清理"""
+ self.stop_watchdog()
+ self.restore_priority()
+ self.release_single_instance()
+ self._execute_shutdown_hooks()
+ self._is_protected = False
+
+ def get_status(self) -> dict:
+ """获取当前保护状态"""
+ return {
+ "platform": PLATFORM,
+ "is_protected": self._is_protected,
+ "single_instance_held": (
+ self._lock_fd is not None or
+ hasattr(self, '_win_mutex_handle') and self._win_mutex_handle is not None
+ ),
+ "watchdog_running": (
+ self._watchdog_thread is not None and
+ self._watchdog_thread.is_alive()
+ ),
+ "shutdown_hooks_count": len(self._shutdown_hooks),
+ }
+
+
+# ── 全局单例 ──
+_protection: Optional[ProcessProtection] = None
+
+
+def get_protection() -> ProcessProtection:
+ """获取全局进程保护器单例"""
+ global _protection
+ if _protection is None:
+ _protection = ProcessProtection()
+ return _protection
+
+
+def enable_process_protection(priority_level: str = "above_normal",
+ watchdog: bool = True,
+ restart_on_crash: bool = False) -> dict:
+ """
+ 快捷函数:启用进程保护
+
+ Returns:
+ 各项保护状态字典
+ """
+ return get_protection().enable_all(
+ priority_level=priority_level,
+ watchdog=watchdog,
+ restart_on_crash=restart_on_crash,
+ )
+
+
+def disable_process_protection():
+ """快捷函数:禁用进程保护"""
+ get_protection().disable_all()
+
+
+def get_protection_status() -> dict:
+ """快捷函数:获取保护状态"""
+ return get_protection().get_status()
+
+
+# ── CLI 入口(可独立运行测试) ──
+if __name__ == "__main__":
+ import json
+
+ print(f"平台: {PLATFORM}")
+ print(f"PID: {os.getpid()}")
+ print()
+
+ prot = ProcessProtection("xiaoli-cli-test")
+
+ # 测试单实例
+ is_first = prot.acquire_single_instance()
+ print(f"单实例锁: {'获取成功 (首个实例)' if is_first else '获取失败 (已有实例)'}")
+
+ if is_first:
+ # 测试优先级提升
+ prio_ok = prot.elevate_priority("above_normal")
+ print(f"优先级提升: {'成功' if prio_ok else '失败'}")
+
+ # 测试 PPL 保护
+ ppl_ok = prot.enable_ppl_protection()
+ print(f"PPL 保护: {'启用' if ppl_ok else '失败'}")
+
+ # 测试看门狗
+ prot.start_watchdog(check_interval=2.0)
+ print(f"看门狗: 已启动")
+
+ # 显示状态
+ status = prot.get_status()
+ print(f"\n状态: {json.dumps(status, indent=2, ensure_ascii=False)}")
+
+ print("\n按 Ctrl+C 退出...")
+ try:
+ while True:
+ time.sleep(1)
+ except KeyboardInterrupt:
+ print("\n正在清理...")
+ prot.disable_all()
+ print("完成")