Files
SenSu/main.py
T
qinglong c20417ff93 feat: 更新服务 — ZIP下载/备份/回滚/重启
UpdateService:
- 分支感知: 从 base_config 读取 branch, 拼接 archive/{branch}.zip
- 定时检查: 每6h 拉取 version.json 比对版本
- 更新前自动打包备份 (zip, 保留最新5个)
- 保护用户配置: base_config.yaml / plugins/*/config.yaml / data/
- 子进程 pip install 依赖

API:
- GET /api/updates — 更新状态
- POST /api/updates/check — 手动检查
- POST /api/updates/apply — 执行更新

回滚:
- python main.py --rollback  — 恢复到最新备份
- 无备份时提示

重启:
- .restart_flag 信号文件 → 优雅退出 → systemd 拉起

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 20:32:42 +08:00

552 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import asyncio
import sys
import signal
import time
import argparse
from services.project_engine import ProjectEngine
from services.pyenv_manager import PyEnvManager
from services.proxy_service import ProxyService
from services.web_panel.utils.system_info import SystemInfoCollector
from services.sensu_db import SenSuDB
from services.update_service import UpdateService
from services.update_service import UpdateService
import os
from pathlib import Path
# 添加项目根目录到Python路径
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
from services.init_service import InitService
from services.log_service import LogService
from services.tui_service import TuiService
from services.command_service import CommandService
from services.auth_service import AuthService
from services.internet_service import InternetService
from services.plugin_service import PluginService
from services.permission_service import PermissionService
from services.api_service import APIService
from services.shutdown_service import ShutdownService
from services.web_panel.manager import WebPanelManager
from bridges.core_bridge import CoreBridge
from bridges.plugin_bridge import PluginBridge
from service_manager import ServiceManager
logger = logging.getLogger(__name__)
class SenSuFramework:
"""框架主类"""
def __init__(self, headless=False):
self.service_manager = ServiceManager()
self.is_running = False
self.headless = headless
logger.debug("🐱 SenSu 框架初始化开始")
async def initialize(self):
"""初始化框架"""
try:
logger.info("🐱 SenSu 启动中...")
# 1. 初始化服务
init_service = InitService()
configs = await init_service.initialize_framework()
base_config = configs['base']
self.service_manager.register_service("init", init_service)
# 2. 日志服务
log_service = LogService(base_config)
self.service_manager.register_service("log", log_service)
# 2.5 自动启动脚本 (日志服务就绪后)
await init_service.start_auto_scripts()
# 2.6 数据库 (持久化层)
sensu_db = SenSuDB()
self.service_manager.register_service("sensu_db", sensu_db)
# 2.7 系统信息采集器 (共享实例,TUI + Web面板共用)
sys_collector = SystemInfoCollector()
self.service_manager.register_service("sys_collector", sys_collector)
# 3. 核心桥接服务
core_bridge = CoreBridge()
await core_bridge.start()
self.service_manager.register_service("core_bridge", core_bridge)
# 4. 指令服务
command_service = CommandService(self.service_manager)
command_service.register_builtin_commands()
self.service_manager.register_service("command", command_service)
# 5. 认证服务
logger.info("> 初始化 访问认证 ...")
auth_service = AuthService(base_config)
self.service_manager.register_service("auth", auth_service)
# 6. 插件桥接服务
logger.info("> 初始化 插件桥 ...")
plugin_bridge = PluginBridge(core_bridge)
await plugin_bridge.start()
self.service_manager.register_service("plugin_bridge", plugin_bridge)
# 7. 关闭服务
logger.info("> 初始化 关闭服务 中...")
shutdown_service = ShutdownService(self.service_manager)
self.service_manager.register_service("shutdown", shutdown_service)
# 8. TUI服务
if self.headless:
logger.info("Headless mode, skip TUI")
self.service_manager.register_service("tui", self._create_fallback_tui())
else:
try:
tui_service = TuiService(
base_config, log_service, command_service,
sys_collector=sys_collector,
)
await tui_service.start()
self.service_manager.register_service("tui", tui_service)
logger.info("TUI服务启动成功")
except Exception as e:
logger.warning(f"TUI服务启动失败: {e}")
self.service_manager.register_service("tui", self._create_fallback_tui())
# 9. 权限服务
logger.info("> 初始化 权限服务 中...")
permission_service = PermissionService(base_config, self.service_manager.get_service("tui"), core_bridge)
permission_started = await permission_service.start()
if permission_started:
self.service_manager.register_service("permission", permission_service)
else:
logger.warning("权限服务启动失败,跳过注册")
# 10. 互联网服务(提前创建但不启动)
logger.info("> 初始化 网络服务 中...")
internet_service = InternetService(base_config, self.service_manager)
self.service_manager.register_service("internet", internet_service)
# 11. 插件服务(在网络服务启动前注册路由)
logger.info("> 初始化 插件管理器 中...")
plugin_service = PluginService(base_config, permission_service, plugin_bridge, self.service_manager)
await plugin_service.start()
self.service_manager.register_service("plugin", plugin_service)
# 将 plugin_service 注入 TUI 插件状态面板
tui = self.service_manager.get_service("tui")
if hasattr(tui, 'tui_app') and tui.tui_app:
tui.tui_app.plugin_panel.set_plugin_service(plugin_service)
# === 🟢 新增: 11.5 Web管理面板初始化 (必须在网络服务启动前!) ===
# 原因: aiohttp 启动后会“冻结”路由器,之后再挂载子应用会报错
logger.info("> 初始化 Web 管理面板 中...")
try:
# 确保 internet_service 已经实例化(在第10步)
if internet_service:
from services.web_panel.manager import WebPanelManager
# 初始化面板管理器
web_panel = WebPanelManager(base_config, self.service_manager)
# 这里的 start() 会把面板路由挂载到 http_app 上 (此时还未冻结)
if await web_panel.start():
self.service_manager.register_service("web_panel", web_panel)
logger.info("✅ Web 面板挂载完成")
else:
logger.warning("Web 面板初始化未完成")
except Exception as e:
logger.error(f"❌ Web 管理面板初始化失败: {str(e)}", exc_info=True)
# 12. 启动网络服务(在所有插件路由注册后)
logger.info("> 启动网络服务 中...")
try:
# 先检查依赖和端口
health_info = await internet_service.check_service_health()
logger.debug(f"网络服务预检查: {health_info}")
if not health_info["dependencies_available"]:
logger.error("❌ 缺少必要的依赖包,网络服务无法启动")
logger.info("💡 请运行: pip install aiohttp yaml")
internet_started = False
else:
# 现在启动网络服务(路由器会在启动时冻结)
internet_started = await internet_service.start()
# 再次检查服务状态
if internet_started:
post_health = await internet_service.check_service_health()
logger.debug(f"网络服务启动后检查: {post_health}")
if post_health["http_active"]:
logger.info("✅ 互联网服务启动成功")
# 显示服务信息
logger.info(f"🌐 服务地址: http://{internet_service.http_host}:{internet_service.http_port}")
logger.info(f"🔍 健康检查: http://{internet_service.http_host}:{internet_service.http_port}/health")
else:
logger.warning("⚠️ 网络服务已启动但端口未响应")
else:
logger.warning("❌ 互联网服务启动失败")
except Exception as e:
logger.error(f"❌ 互联网服务启动异常: {str(e)}", exc_info=True)
logger.warning("互联网服务启动失败")
# 13. API服务
logger.info("> 初始化 API 服务 中...")
try:
api_service = APIService(internet_service, auth_service, permission_service)
self.service_manager.register_service("api", api_service)
logger.info("✅ API 服务启动成功")
except Exception as e:
logger.error(f"❌ API 服务初始化失败: {str(e)}")
logger.warning("API 服务启动失败,跳过注册")
# 注册框架关闭处理器
shutdown_service.register_shutdown_handler(self._framework_shutdown_handler)
# 11.7 项目引擎 + 反向代理
try:
logger.info("> 初始化 项目引擎 中...")
project_engine = ProjectEngine(self.service_manager)
await project_engine.start()
self.service_manager.register_service("project_engine", project_engine)
pe_mgr = PyEnvManager("data/projects")
self.service_manager.register_service("pyenv", pe_mgr)
logger.info("> 初始化 反向代理 中...")
proxy_service = ProxyService(self.service_manager)
await proxy_service.start()
self.service_manager.register_service("proxy", proxy_service)
logger.info("✅ 项目引擎+反向代理就绪")
logger.info("> 初始化 更新检测 中...")
update_service = UpdateService(self.service_manager)
await update_service.start()
self.service_manager.register_service("update", update_service)
logger.info("✅ 更新检测就绪")
except Exception as e:
logger.warning(f"引擎/代理初始化跳过: {e}")
logger.info("🎉 SenSu 初始化完成!")
self.service_manager.start_time = time.time()
self.is_running = True
# 显示欢迎日志
version = base_config.get('framework', {}).get('version', 'Unknown')
logLevel = base_config.get('logging', {}).get('level', 'Unknown')
logger.info("\n🐱 SenSu 已就绪!")
logger.info(f"当前 SenSu 版本号 {version}")
logger.info(f"当前日志级别 {logLevel}")
panel_path = base_config.get('panel', {}).get('entrance', {}).get('path', 'panel')
# 显示欢迎消息
tui_service = self.service_manager.get_service("tui")
if hasattr(tui_service, 'show_message'):
tui_service.show_message("🐱 SenSu 框架 已就绪!\n", "info")
tui_service.show_message("====================================\n", "info")
tui_service.show_message("🐱 SenSu 已就绪!", "info")
tui_service.show_message(f"当前 SenSu 版本号 {version}", "info")
tui_service.show_message(f"当前日志级别 {logLevel}\n", "info")
tui_service.show_message("====================================\n", "info")
tui_service.show_message(f"当前API配置地址", "debug")
tui_service.show_message(f"地址:http://{internet_service.http_host}:{internet_service.http_port}", "debug")
tui_service.show_message("====================================\n", "debug")
tui_service.show_message(f"🌐 Web 面板配置地址: http://{internet_service.http_host}:{internet_service.http_port}{panel_path}", "info")
else:
print("🐱 SenSu 已就绪!输入 'help' 查看可用命令")
except Exception as e:
logger.error(f"框架初始化失败: {str(e)}", exc_info=True)
await self._safe_shutdown()
raise
def _create_fallback_tui(self):
"""创建回退的TUI服务(命令行模式)"""
class FallbackTuiService:
def __init__(self):
self.is_running = True
def show_message(self, message: str, msg_type: str = "info", persistent: bool = False):
"""显示消息到控制台"""
prefix = {
"info": "",
"warning": "⚠️",
"error": "",
"success": ""
}.get(msg_type, "📝")
print(f"{prefix} {message}")
async def start(self):
"""启动回退TUI"""
print("🐱 使用命令行模式...")
return True
def shutdown(self):
"""关闭回退TUI"""
self.is_running = False
return FallbackTuiService()
async def _safe_shutdown(self):
"""安全关闭,即使服务未完全初始化"""
try:
logger.info("执行安全关闭")
self.is_running = False
# 尝试获取关闭服务
try:
shutdown_service = self.service_manager.get_service("shutdown")
if shutdown_service:
await shutdown_service.initiate_shutdown("安全关闭")
return
except:
pass
# 如果关闭服务不可用,手动关闭其他服务
services_to_shutdown = ['plugin', 'core_bridge', 'plugin_bridge', 'tui', 'log']
for service_name in services_to_shutdown:
try:
service = self.service_manager.get_service(service_name)
if service and hasattr(service, 'shutdown'):
await service.shutdown() if asyncio.iscoroutinefunction(service.shutdown) else service.shutdown()
except:
pass
logger.debug("安全关闭完成")
except Exception as e:
print(f"❌ 安全关闭时出错: {e}")
async def _framework_shutdown_handler(self):
"""框架关闭处理器"""
try:
logger.info("执行框架关闭处理")
self.is_running = False
# 关闭插件服务
plugin_service = self.service_manager.get_service("plugin")
if plugin_service:
# 卸载所有插件
for plugin_name in list(plugin_service.plugins.keys()):
await plugin_service.unload_plugin(plugin_name)
# 关闭桥接服务
core_bridge = self.service_manager.get_service("core_bridge")
if core_bridge:
await core_bridge.shutdown()
plugin_bridge = self.service_manager.get_service("plugin_bridge")
if plugin_bridge:
await plugin_bridge.shutdown()
logger.debug("框架关闭处理完成")
except Exception as e:
logger.error(f"框架关闭处理时出错: {str(e)}", exc_info=True)
async def run(self):
"""运行框架主循环"""
try:
logger.info("进入框架主循环")
# 特殊终端,添加命令行输入处理
tui = self.service_manager.get_service("tui")
if self.headless:
import os as _os
restart_flag = _os.path.join(_os.path.dirname(__file__), ".restart_flag")
while self.is_running:
if _os.path.exists(restart_flag):
logger.info("🔄 检测到重启标记, 执行优雅重启...")
_os.remove(restart_flag)
await self._restart_framework()
break
try: await asyncio.sleep(1)
except asyncio.CancelledError: break
elif not hasattr(tui, 'tui_app'):
await self._run_cli_mode()
else:
# 原有的TUI模式
while self.is_running:
try:
await asyncio.sleep(1)
except asyncio.CancelledError:
logger.info("主循环被取消")
break
except Exception as e:
logger.error(f"主循环运行时出错: {str(e)}", exc_info=True)
await asyncio.sleep(5)
logger.info("框架主循环结束")
except Exception as e:
logger.error(f"运行框架主循环时出错: {str(e)}", exc_info=True)
await self._safe_shutdown()
async def _restart_framework(self):
"""优雅重启: 关闭所有服务后退出 (由 systemd/supervisor 自动拉起)"""
logger.info("🔄 执行优雅重启...")
await self.shutdown()
import sys as _sys
_sys.exit(0)
async def _run_cli_mode(self):
"""运行命令行模式"""
try:
print("🐱 进入命令行模式,输入 'exit' 退出")
command_service = self.service_manager.get_service("command")
while self.is_running:
try:
# 读取用户输入
user_input = await asyncio.get_event_loop().run_in_executor(
None, input, "🐱 > "
)
if user_input.strip().lower() in ('exit', 'quit', 'q'):
await self.shutdown()
break
# 处理命令
if user_input.strip():
result = await command_service.process_command(user_input, "cli")
print(f"📝 {result}")
except (KeyboardInterrupt, EOFError):
print("\n🐱 接收到退出信号")
await self.shutdown()
break
except Exception as e:
print(f"❌ 命令处理错误: {str(e)}")
except Exception as e:
logger.error(f"命令行模式运行时出错: {str(e)}", exc_info=True)
await self._safe_shutdown()
async def shutdown(self):
"""关闭框架"""
try:
logger.info("开始关闭框架")
self.is_running = False
# 通过关闭服务发起优雅关闭
shutdown_service = self.service_manager.get_service("shutdown")
if shutdown_service:
await shutdown_service.initiate_shutdown("手动关闭")
else:
await self._safe_shutdown()
except Exception as e:
logger.error(f"关闭框架时出错: {str(e)}", exc_info=True)
await self._safe_shutdown()
def rollback():
"""回滚到最新备份"""
import zipfile, shutil, os, glob
backup_dir = os.path.join(os.path.dirname(__file__), "data", "backups")
if not os.path.isdir(backup_dir):
print("📭 没有备份目录")
return
backups = sorted(glob.glob(os.path.join(backup_dir, "SenSu_backup_*.zip")), key=os.path.getmtime, reverse=True)
if not backups:
print("📭 没有可用的备份包")
return
latest = backups[0]
name = os.path.basename(latest)
print(f"📦 回滚到: {name}")
try:
import tempfile
with tempfile.TemporaryDirectory() as tmp:
with zipfile.ZipFile(latest, "r") as zf:
zf.extractall(tmp)
project_root = os.path.dirname(__file__)
for item in os.listdir(tmp):
src = os.path.join(tmp, item)
dst = os.path.join(project_root, item)
if os.path.isdir(src):
shutil.copytree(src, dst, dirs_exist_ok=True)
else:
shutil.copy2(src, dst)
print("✅ 回滚完成, 请重启 SenSu")
except Exception as e:
print(f"❌ 回滚失败: {e}")
async def main(headless=False):
"""主函数"""
framework = SenSuFramework(headless=headless)
try:
# 初始化框架
await framework.initialize()
# 运行主循环
await framework.run()
except KeyboardInterrupt:
print("\n🐱 接收到键盘中断")
await framework.shutdown()
except Exception as e:
print(f"🐱 框架运行出错: {str(e)}")
await framework._safe_shutdown()
sys.exit(1)
if __name__ == "__main__":
try:
# 设置更详细的异常处理
import signal
def signal_handler(signum, frame):
"""信号处理"""
print(f"\n🐱 接收到信号 {signum},正在关闭...")
sys.exit(0)
# 注册信号处理
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
print("\n🐱 主程序启动...")
# 运行主程序
parser = argparse.ArgumentParser(description="SenSu")
parser.add_argument("--headless", action="store_true")
parser.add_argument("--rollback", action="store_true", help="回滚到最新备份")
args = parser.parse_args()
if args.rollback:
rollback()
sys.exit(0)
asyncio.run(main(headless=args.headless))
except KeyboardInterrupt:
print("\n🐱 接收到键盘中断,关闭...")
print("🐱 再见喵~")
sys.exit(0)
except SystemExit as e:
# 优雅处理SystemExit
exit_code = e.code if e.code is not None else 0
if exit_code == 0:
print("🐱 框架已关闭")
else:
print(f"🐱 框架退出,代码: {exit_code}")
sys.exit(exit_code)
except Exception as e:
print(f"🐱 框架运行异常: {str(e)}")
import traceback
traceback.print_exc()
sys.exit(1)
finally:
print("🐱 框架进程结束")