feat: 升级流程增强 + 网关化配置 + 重连循环化

- 配置化:gateway.url/socket.path/download.prefix(网关基础地址统一推导 ws 与下载地址)、
  main.home 主目录(jar/pid 相对它推导)、order.basic;properties 中文 \uXXXX 转义自动还原
- 升级流程:下载 PK 魔数检查与诊断日志;SHA 校验;替换改「改名腾位」绕开 Windows 文件锁延迟;
  nssm stop/start 驱动停止与拉起(pid 强杀回退);升级中标志暂停看门狗拉起避免抢占 jar 锁
- ws_client:重连改循环实现修复递归栈溢出(长时间断线后 maximum recursion depth);
  退避封顶延长至 5 分钟;心跳线程独立修复「暂无心跳数据」
- 新增命令:GET_APIS/HTTP_PROXY(接口测试代理,纯透传鉴权)、GET_LOG_DOWNLOAD(日志下载备用)
- 诊断增强:taskkill/进程退出/替换重试逐次日志

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-03 16:52:34 +08:00
co-authored by Claude
parent b572697e17
commit 0b13de14eb
7 changed files with 366 additions and 152 deletions
+62 -1
View File
@@ -12,6 +12,11 @@ import threading
log = logging.getLogger(__name__)
# 模块级「升级中」标志:升级流程开始时置 True,结束后置 False。
# 看门狗探测到主程序不健康时若此标志为 True,说明是升级主动停的,跳过拉起,
# 避免升级替换 jar 时被看门狗拉起的新进程抢占文件锁。
_upgrading = threading.Event()
class ProcessGuard:
"""看门狗:周期健康探测 + 宕机拉起 + 告警上报。"""
@@ -52,12 +57,18 @@ class ProcessGuard:
log.warning("守护探测异常:%s", e)
def _check(self):
"""单次探测:健康则清零计数;连续 3 次失败则拉起。"""
"""单次探测:健康则清零计数;连续 3 次失败则拉起(升级期间跳过)"""
health = self.main_app_client.health()
if health.get("ok") is True:
self._fail_count = 0
return
# 升级流程主动停的主程序,看门狗不接管,避免抢占 jar 锁
if _upgrading.is_set():
log.info("主程序不可达,但升级流程进行中,看门狗跳过拉起")
self._fail_count = 0 # 不累计,升级结束后自然恢复
return
self._fail_count += 1
log.warning("主程序健康探测失败(%d/3):%s", self._fail_count, health.get("error"))
if self._fail_count < 3:
@@ -129,6 +140,56 @@ def restart_main_service(nssm_path: str, service_name: str) -> tuple[bool, str]:
return False, str(e)
def stop_main_service(nssm_path: str, service_name: str) -> tuple[bool, str]:
"""
通过 NSSM 停止主程序服务(nssm stop 会等待服务进程退出后返回)。
用于升级流程:停止成功即文件锁释放,替换 jar 不再被 WinError 32 挡住。
nssm_path 为 nssm.exe 完整路径;service_name 为注册的 Windows 服务名。
返回 (是否成功, 输出/原因)。
"""
try:
proc = subprocess.run(
[nssm_path, "stop", service_name],
capture_output=True,
timeout=120,
)
out = ((proc.stdout or b"") + (proc.stderr or b"")).decode("utf-16", errors="replace").strip()
ok = proc.returncode == 0
if ok:
log.info("nssm stop %s 成功:%s", service_name, out or "(无输出)")
else:
log.warning("nssm stop %s 失败(exit=%s):%s", service_name, proc.returncode, out)
return ok, out
except Exception as e:
log.warning("nssm stop %s 执行异常:%s", service_name, e)
return False, str(e)
def start_main_service(nssm_path: str, service_name: str) -> tuple[bool, str]:
"""
通过 NSSM 启动主程序服务(nssm start 触发服务进入运行态,应用初始化由健康检查轮询确认)。
返回 (是否成功, 输出/原因)。
"""
try:
proc = subprocess.run(
[nssm_path, "start", service_name],
capture_output=True,
timeout=60,
)
out = ((proc.stdout or b"") + (proc.stderr or b"")).decode("utf-16", errors="replace").strip()
ok = proc.returncode == 0
if ok:
log.info("nssm start %s 成功:%s", service_name, out or "(无输出)")
else:
log.warning("nssm start %s 失败(exit=%s):%s", service_name, proc.returncode, out)
return ok, out
except Exception as e:
log.warning("nssm start %s 执行异常:%s", service_name, e)
return False, str(e)
def start_main_app(start_cmd: str) -> subprocess.Popen | None:
"""
拉起主程序。