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>
This commit is contained in:
@@ -12,6 +12,8 @@ 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
|
||||
|
||||
@@ -223,6 +225,13 @@ class SenSuFramework:
|
||||
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}")
|
||||
|
||||
@@ -355,7 +364,14 @@ class SenSuFramework:
|
||||
# 特殊终端,添加命令行输入处理
|
||||
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'):
|
||||
@@ -377,6 +393,13 @@ class SenSuFramework:
|
||||
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):
|
||||
"""运行命令行模式"""
|
||||
@@ -429,6 +452,38 @@ class SenSuFramework:
|
||||
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)
|
||||
@@ -466,7 +521,13 @@ if __name__ == "__main__":
|
||||
# 运行主程序
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user