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:
@@ -2,6 +2,8 @@ framework:
|
|||||||
debug: true
|
debug: true
|
||||||
name: SenSu
|
name: SenSu
|
||||||
version: v0.7.0
|
version: v0.7.0
|
||||||
|
branch: dev
|
||||||
|
repo_url: https://git.yeij.top/AskaEth/SenSu
|
||||||
logging:
|
logging:
|
||||||
debug_level_file: true
|
debug_level_file: true
|
||||||
level: DEBUG
|
level: DEBUG
|
||||||
|
|||||||
@@ -482,5 +482,49 @@
|
|||||||
"framework.command.execute"
|
"framework.command.execute"
|
||||||
],
|
],
|
||||||
"timestamp": 29386.802027486
|
"timestamp": 29386.802027486
|
||||||
|
},
|
||||||
|
"1daf5e1a": {
|
||||||
|
"plugin_name": "example_plugin",
|
||||||
|
"permissions": [
|
||||||
|
"plugin.example.read",
|
||||||
|
"plugin.example.write",
|
||||||
|
"plugin.example.execute",
|
||||||
|
"framework.event.subscribe",
|
||||||
|
"framework.command.execute"
|
||||||
|
],
|
||||||
|
"timestamp": 29765.65822229
|
||||||
|
},
|
||||||
|
"b2d1dd2e": {
|
||||||
|
"plugin_name": "sentinel",
|
||||||
|
"permissions": [
|
||||||
|
"plugin.sentinel.read",
|
||||||
|
"plugin.sentinel.write",
|
||||||
|
"plugin.network.access",
|
||||||
|
"framework.event.subscribe",
|
||||||
|
"framework.command.execute"
|
||||||
|
],
|
||||||
|
"timestamp": 29765.666844425
|
||||||
|
},
|
||||||
|
"cbc42132": {
|
||||||
|
"plugin_name": "example_plugin",
|
||||||
|
"permissions": [
|
||||||
|
"plugin.example.read",
|
||||||
|
"plugin.example.write",
|
||||||
|
"plugin.example.execute",
|
||||||
|
"framework.event.subscribe",
|
||||||
|
"framework.command.execute"
|
||||||
|
],
|
||||||
|
"timestamp": 35916.606768589
|
||||||
|
},
|
||||||
|
"2bd9a945": {
|
||||||
|
"plugin_name": "sentinel",
|
||||||
|
"permissions": [
|
||||||
|
"plugin.sentinel.read",
|
||||||
|
"plugin.sentinel.write",
|
||||||
|
"plugin.network.access",
|
||||||
|
"framework.event.subscribe",
|
||||||
|
"framework.command.execute"
|
||||||
|
],
|
||||||
|
"timestamp": 35916.627558381
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ commands:
|
|||||||
permissions:
|
permissions:
|
||||||
- framework.command.test
|
- framework.command.test
|
||||||
source: internal
|
source: internal
|
||||||
last_updated: 29386.77279457
|
last_updated: 35916.559043798
|
||||||
plugin_commands:
|
plugin_commands:
|
||||||
example_plugin:
|
example_plugin:
|
||||||
echo: *id001
|
echo: *id001
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
http_port: 4200
|
http_port: 4200
|
||||||
last_updated: 29386.797962851
|
last_updated: 35916.617063641
|
||||||
plugin_routes:
|
plugin_routes:
|
||||||
example_plugin:
|
example_plugin:
|
||||||
- methods:
|
- methods:
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ from services.pyenv_manager import PyEnvManager
|
|||||||
from services.proxy_service import ProxyService
|
from services.proxy_service import ProxyService
|
||||||
from services.web_panel.utils.system_info import SystemInfoCollector
|
from services.web_panel.utils.system_info import SystemInfoCollector
|
||||||
from services.sensu_db import SenSuDB
|
from services.sensu_db import SenSuDB
|
||||||
|
from services.update_service import UpdateService
|
||||||
|
from services.update_service import UpdateService
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -223,6 +225,13 @@ class SenSuFramework:
|
|||||||
await proxy_service.start()
|
await proxy_service.start()
|
||||||
self.service_manager.register_service("proxy", proxy_service)
|
self.service_manager.register_service("proxy", proxy_service)
|
||||||
logger.info("✅ 项目引擎+反向代理就绪")
|
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:
|
except Exception as e:
|
||||||
logger.warning(f"引擎/代理初始化跳过: {e}")
|
logger.warning(f"引擎/代理初始化跳过: {e}")
|
||||||
|
|
||||||
@@ -355,7 +364,14 @@ class SenSuFramework:
|
|||||||
# 特殊终端,添加命令行输入处理
|
# 特殊终端,添加命令行输入处理
|
||||||
tui = self.service_manager.get_service("tui")
|
tui = self.service_manager.get_service("tui")
|
||||||
if self.headless:
|
if self.headless:
|
||||||
|
import os as _os
|
||||||
|
restart_flag = _os.path.join(_os.path.dirname(__file__), ".restart_flag")
|
||||||
while self.is_running:
|
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)
|
try: await asyncio.sleep(1)
|
||||||
except asyncio.CancelledError: break
|
except asyncio.CancelledError: break
|
||||||
elif not hasattr(tui, 'tui_app'):
|
elif not hasattr(tui, 'tui_app'):
|
||||||
@@ -378,6 +394,13 @@ class SenSuFramework:
|
|||||||
logger.error(f"运行框架主循环时出错: {str(e)}", exc_info=True)
|
logger.error(f"运行框架主循环时出错: {str(e)}", exc_info=True)
|
||||||
await self._safe_shutdown()
|
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):
|
async def _run_cli_mode(self):
|
||||||
"""运行命令行模式"""
|
"""运行命令行模式"""
|
||||||
try:
|
try:
|
||||||
@@ -429,6 +452,38 @@ class SenSuFramework:
|
|||||||
logger.error(f"关闭框架时出错: {str(e)}", exc_info=True)
|
logger.error(f"关闭框架时出错: {str(e)}", exc_info=True)
|
||||||
await self._safe_shutdown()
|
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):
|
async def main(headless=False):
|
||||||
"""主函数"""
|
"""主函数"""
|
||||||
framework = SenSuFramework(headless=headless)
|
framework = SenSuFramework(headless=headless)
|
||||||
@@ -466,7 +521,13 @@ if __name__ == "__main__":
|
|||||||
# 运行主程序
|
# 运行主程序
|
||||||
parser = argparse.ArgumentParser(description="SenSu")
|
parser = argparse.ArgumentParser(description="SenSu")
|
||||||
parser.add_argument("--headless", action="store_true")
|
parser.add_argument("--headless", action="store_true")
|
||||||
|
parser.add_argument("--rollback", action="store_true", help="回滚到最新备份")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.rollback:
|
||||||
|
rollback()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
asyncio.run(main(headless=args.headless))
|
asyncio.run(main(headless=args.headless))
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""版本更新服务 — 下载 release ZIP + 解压 + 保护配置 + 热重启"""
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import urllib.request
|
||||||
|
import zipfile
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CHECK_INTERVAL = 3600 * 6 # 6 小时
|
||||||
|
_PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
_RESTART_FLAG = _PROJECT_ROOT / ".restart_flag"
|
||||||
|
|
||||||
|
# 更新时跳过的路径 (用户配置/数据)
|
||||||
|
_PRESERVE_PATHS = {
|
||||||
|
"config/framework/base_config.yaml",
|
||||||
|
"config/permissions/",
|
||||||
|
"data/",
|
||||||
|
"plugins/*/config.yaml",
|
||||||
|
"logs/",
|
||||||
|
".restart_flag",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_ver(v: str) -> tuple:
|
||||||
|
v = v.strip().lstrip("vV")
|
||||||
|
try:
|
||||||
|
return tuple(int(p) for p in v.split(".")[:3])
|
||||||
|
except Exception:
|
||||||
|
return (0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
_BACKUP_SKIP = {"data/", "logs/", "__pycache__/", ".git/", "node_modules/", ".venv/", "venv/",
|
||||||
|
"*.pyc", "*.pyo", ".restart_flag"}
|
||||||
|
|
||||||
|
|
||||||
|
def _backup_current(dest_path: Path):
|
||||||
|
"""将当前项目打包为 ZIP (跳过数据/日志/缓存)"""
|
||||||
|
from fnmatch import fnmatch
|
||||||
|
with zipfile.ZipFile(str(dest_path), "w", zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
for root, dirs, files in os.walk(str(_PROJECT_ROOT)):
|
||||||
|
# 跳过不需要备份的目录
|
||||||
|
dirs[:] = [d for d in dirs if not any(
|
||||||
|
fnmatch(d + "/", p.rstrip("/") + "/") or fnmatch(d, p.rstrip("/"))
|
||||||
|
for p in _BACKUP_SKIP if "/" in p or p.endswith("/")
|
||||||
|
) and d not in {p.rstrip("/") for p in _BACKUP_SKIP if not any(c in p for c in "*?[")}]
|
||||||
|
for f in files:
|
||||||
|
rel = os.path.relpath(os.path.join(root, f), str(_PROJECT_ROOT))
|
||||||
|
if any(fnmatch(f, p) or fnmatch(rel, p) for p in _BACKUP_SKIP):
|
||||||
|
continue
|
||||||
|
zf.write(os.path.join(root, f), rel)
|
||||||
|
|
||||||
|
|
||||||
|
def _should_preserve(rel_path: str) -> bool:
|
||||||
|
"""检查路径是否应跳过 (保护用户数据)"""
|
||||||
|
from fnmatch import fnmatch
|
||||||
|
for pattern in _PRESERVE_PATHS:
|
||||||
|
if fnmatch(rel_path, pattern) or rel_path.startswith(pattern.rstrip("/") + "/"):
|
||||||
|
return True
|
||||||
|
# 匹配目录前缀
|
||||||
|
if rel_path == pattern.rstrip("/"):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateService:
|
||||||
|
def __init__(self, service_manager):
|
||||||
|
self.sm = service_manager
|
||||||
|
self._task: Optional[asyncio.Task] = None
|
||||||
|
self._running = False
|
||||||
|
self.status: dict = {"state": "idle", "progress": 0, "error": "",
|
||||||
|
"last_check": 0, "update_available": False}
|
||||||
|
# 从配置读取分支和仓库地址
|
||||||
|
try:
|
||||||
|
init = self.sm.get_service("init")
|
||||||
|
cfg = init.get_config("base").get("framework", {})
|
||||||
|
except Exception:
|
||||||
|
cfg = {}
|
||||||
|
self.branch = cfg.get("branch", "main")
|
||||||
|
self.repo_url = cfg.get("repo_url", "https://git.yeij.top/AskaEth/SenSu").rstrip("/")
|
||||||
|
self.repo_name = self.repo_url.rstrip("/").split("/")[-1]
|
||||||
|
# ZIP 下载地址: Gitea 格式
|
||||||
|
self.download_url = f"{self.repo_url}/archive/{self.branch}.zip"
|
||||||
|
# 版本检查 API
|
||||||
|
self.version_url = f"{self.repo_url}/raw/branch/{self.branch}/version.json"
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
self._running = True
|
||||||
|
self._task = asyncio.create_task(self._loop())
|
||||||
|
logger.info(f"🔄 更新服务已启动 — 分支: {self.branch}, 仓库: {self.repo_url}")
|
||||||
|
|
||||||
|
async def _loop(self):
|
||||||
|
await asyncio.sleep(30) # 等框架就绪
|
||||||
|
while self._running:
|
||||||
|
await self._check_version()
|
||||||
|
await asyncio.sleep(CHECK_INTERVAL)
|
||||||
|
|
||||||
|
async def _check_version(self):
|
||||||
|
"""检查是否有新版本 (非阻塞)"""
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
data = await loop.run_in_executor(None, self._sync_fetch_json, self.version_url)
|
||||||
|
self.status["last_check"] = time.time()
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
remote_fw = data.get("framework", {})
|
||||||
|
remote_ver = remote_fw.get("version", "")
|
||||||
|
if not remote_ver:
|
||||||
|
return
|
||||||
|
current = self._current_version()
|
||||||
|
if _parse_ver(remote_ver) > _parse_ver(current):
|
||||||
|
self.status["update_available"] = True
|
||||||
|
self.status["latest_version"] = remote_ver
|
||||||
|
self.status["current_version"] = current
|
||||||
|
self.status["download_url"] = self.download_url
|
||||||
|
logger.info(f"🔄 新版本可用: {remote_ver} (当前: {current})")
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"版本检查失败: {e}")
|
||||||
|
|
||||||
|
def _sync_fetch_json(self, url: str) -> Optional[dict]:
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": "SenSu-Update/1.0"})
|
||||||
|
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||||
|
return json.loads(resp.read().decode())
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _current_version(self) -> str:
|
||||||
|
try:
|
||||||
|
init = self.sm.get_service("init")
|
||||||
|
return init.get_config("base").get("framework", {}).get("version", "0.0.0").lstrip("vV")
|
||||||
|
except Exception:
|
||||||
|
return "0.0.0"
|
||||||
|
|
||||||
|
# ── 执行更新 ──
|
||||||
|
|
||||||
|
async def apply_update(self) -> dict:
|
||||||
|
"""下载并应用更新 (在线程池中执行磁盘操作)"""
|
||||||
|
if self.status["state"] == "downloading":
|
||||||
|
return {"ok": False, "error": "更新已在进行中"}
|
||||||
|
|
||||||
|
self.status = {"state": "downloading", "progress": 0, "error": "",
|
||||||
|
"last_check": self.status.get("last_check", 0),
|
||||||
|
"update_available": self.status.get("update_available", False)}
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
result = await loop.run_in_executor(None, self._sync_apply)
|
||||||
|
if result.get("ok"):
|
||||||
|
self.status = {"state": "done", "progress": 100, "error": "",
|
||||||
|
"last_check": time.time(), "update_available": False}
|
||||||
|
# 写重启标记
|
||||||
|
_RESTART_FLAG.touch()
|
||||||
|
logger.info("✅ 更新完成, 框架将在下次主循环检测后重启")
|
||||||
|
else:
|
||||||
|
self.status["state"] = "error"
|
||||||
|
self.status["error"] = result.get("error", "未知错误")
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
self.status["state"] = "error"
|
||||||
|
self.status["error"] = str(e)
|
||||||
|
return {"ok": False, "error": str(e)}
|
||||||
|
|
||||||
|
def _sync_apply(self) -> dict:
|
||||||
|
"""同步执行更新 (在线程池中)"""
|
||||||
|
tmp_zip = None
|
||||||
|
tmp_dir = None
|
||||||
|
try:
|
||||||
|
url = self.download_url
|
||||||
|
logger.info(f"📥 下载更新包: {url}")
|
||||||
|
|
||||||
|
# 0. 备份当前版本
|
||||||
|
self.status["progress"] = 2
|
||||||
|
backup_name = f"SenSu_backup_{self._current_version()}_{time.strftime('%Y%m%d_%H%M%S')}.zip"
|
||||||
|
backup_dir = _PROJECT_ROOT / "data" / "backups"
|
||||||
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
backup_path = backup_dir / backup_name
|
||||||
|
_backup_current(backup_path)
|
||||||
|
logger.info(f"📦 已备份到: {backup_path}")
|
||||||
|
# 清理旧备份 (保留最新 5 个)
|
||||||
|
backups = sorted(backup_dir.glob("SenSu_backup_*.zip"), key=os.path.getmtime, reverse=True)
|
||||||
|
for old in backups[5:]:
|
||||||
|
old.unlink()
|
||||||
|
|
||||||
|
# 1. 下载 ZIP
|
||||||
|
self.status["progress"] = 10
|
||||||
|
tmp_zip = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": "SenSu-Update/1.0"})
|
||||||
|
with urllib.request.urlopen(req, timeout=300) as resp:
|
||||||
|
shutil.copyfileobj(resp, tmp_zip)
|
||||||
|
tmp_zip.close()
|
||||||
|
self.status["progress"] = 40
|
||||||
|
|
||||||
|
# 2. 解压到临时目录
|
||||||
|
tmp_dir = tempfile.mkdtemp()
|
||||||
|
with zipfile.ZipFile(tmp_zip.name, "r") as zf:
|
||||||
|
zf.extractall(tmp_dir)
|
||||||
|
self.status["progress"] = 60
|
||||||
|
|
||||||
|
# 3. Gitea zip 内层目录: 通常是 repo_name-branch/
|
||||||
|
extracted_root = tmp_dir
|
||||||
|
contents = os.listdir(tmp_dir)
|
||||||
|
if len(contents) == 1:
|
||||||
|
inner = os.path.join(tmp_dir, contents[0])
|
||||||
|
if os.path.isdir(inner):
|
||||||
|
extracted_root = inner
|
||||||
|
|
||||||
|
# 4. 覆盖文件 (跳过用户配置)
|
||||||
|
for root, dirs, files in os.walk(extracted_root):
|
||||||
|
rel = os.path.relpath(root, extracted_root)
|
||||||
|
if rel == ".":
|
||||||
|
rel = ""
|
||||||
|
for f in files:
|
||||||
|
fp = os.path.join(rel, f) if rel else f
|
||||||
|
if _should_preserve(fp):
|
||||||
|
continue
|
||||||
|
src = os.path.join(root, f)
|
||||||
|
dst = os.path.join(str(_PROJECT_ROOT), fp)
|
||||||
|
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
|
||||||
|
self.status["progress"] = 80
|
||||||
|
|
||||||
|
# 5. 安装依赖 (子进程)
|
||||||
|
req_file = str(_PROJECT_ROOT / "requirements.txt")
|
||||||
|
if os.path.exists(req_file):
|
||||||
|
r = subprocess.run(
|
||||||
|
["pip", "install", "-r", req_file, "--break-system-packages"],
|
||||||
|
capture_output=True, text=True, timeout=120,
|
||||||
|
cwd=str(_PROJECT_ROOT),
|
||||||
|
)
|
||||||
|
if r.returncode != 0:
|
||||||
|
logger.warning(f"依赖安装警告: {r.stderr[-300:]}")
|
||||||
|
|
||||||
|
self.status["progress"] = 100
|
||||||
|
return {"ok": True, "msg": "更新完成, 框架将重启"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"更新失败: {e}")
|
||||||
|
return {"ok": False, "error": str(e)}
|
||||||
|
finally:
|
||||||
|
if tmp_zip and os.path.exists(tmp_zip.name):
|
||||||
|
os.unlink(tmp_zip.name)
|
||||||
|
if tmp_dir and os.path.exists(tmp_dir):
|
||||||
|
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
async def check_now(self):
|
||||||
|
await self._check_version()
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
self._running = False
|
||||||
|
if self._task:
|
||||||
|
self._task.cancel()
|
||||||
@@ -40,6 +40,9 @@ def setup_routes(app, prefix=''):
|
|||||||
app.router.add_get(f'{prefix}/api/framework', panel_auth(get_framework))
|
app.router.add_get(f'{prefix}/api/framework', panel_auth(get_framework))
|
||||||
app.router.add_get(f'{prefix}/api/system', panel_auth(get_system))
|
app.router.add_get(f'{prefix}/api/system', panel_auth(get_system))
|
||||||
app.router.add_get(f'{prefix}/api/system/ws', _ws_auth_wrapper(sys_ws_handler))
|
app.router.add_get(f'{prefix}/api/system/ws', _ws_auth_wrapper(sys_ws_handler))
|
||||||
|
app.router.add_get(f'{prefix}/api/updates', panel_auth(get_updates))
|
||||||
|
app.router.add_post(f'{prefix}/api/updates/check', panel_auth(check_now))
|
||||||
|
app.router.add_post(f'{prefix}/api/updates/apply', panel_auth(apply_update))
|
||||||
logger.info(f"📡 系统状态WS端点已注册: {prefix}/api/system/ws (已加认证)")
|
logger.info(f"📡 系统状态WS端点已注册: {prefix}/api/system/ws (已加认证)")
|
||||||
|
|
||||||
def _read_version():
|
def _read_version():
|
||||||
@@ -128,3 +131,32 @@ async def sys_ws_handler(req):
|
|||||||
pass
|
pass
|
||||||
_sys_ws_clients.discard(ws)
|
_sys_ws_clients.discard(ws)
|
||||||
return ws
|
return ws
|
||||||
|
|
||||||
|
|
||||||
|
async def get_updates(req):
|
||||||
|
"""获取更新状态"""
|
||||||
|
sm = req.app.get("service_manager")
|
||||||
|
us = sm.get_service("update") if sm else None
|
||||||
|
if not us:
|
||||||
|
return web.json_response({"error": "更新服务未就绪"}, status=503)
|
||||||
|
return web.json_response(us.status)
|
||||||
|
|
||||||
|
|
||||||
|
async def check_now(req):
|
||||||
|
"""手动触发更新检查"""
|
||||||
|
sm = req.app.get("service_manager")
|
||||||
|
us = sm.get_service("update") if sm else None
|
||||||
|
if not us:
|
||||||
|
return web.json_response({"error": "更新服务未就绪"}, status=503)
|
||||||
|
await us.check_now()
|
||||||
|
return web.json_response(us.status)
|
||||||
|
|
||||||
|
|
||||||
|
async def apply_update(req):
|
||||||
|
"""执行更新"""
|
||||||
|
sm = req.app.get("service_manager")
|
||||||
|
us = sm.get_service("update") if sm else None
|
||||||
|
if not us:
|
||||||
|
return web.json_response({"error": "更新服务未就绪"}, status=503)
|
||||||
|
result = await us.apply_update()
|
||||||
|
return web.json_response(result)
|
||||||
|
|||||||
Reference in New Issue
Block a user