feat: RESTART 走 NSSM 服务重启 + 失败原因可观测化

- RESTART 命令优先通过 nssm restart 重启主程序服务(假死场景可用),未配置服务名回退环回 HTTP
- nssm 输出按 UTF-16 解码并记录日志,权限不足时提示需以服务方式运行
- 环回管理接口 code=3001 时明确报鉴权失败,不再误报不可达
- 新增 nssm.path / main.service.name / log.dir 配置项
- 增加文件日志(RotatingFileHandler 5MB×5),服务方式运行可查日志
- HTTP 调用失败与状态码异常统一 WARNING 记录(原 DEBUG 不可见)
- 断线日志区分「网络不通」与「被服务器拒绝(未注册/token 错误)」
- 连接 URL 自动拼接 device.id,消除双处配置不一致

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-27 17:24:17 +08:00
co-authored by Claude
parent 86bbe8ef5a
commit f07bbbc49d
7 changed files with 144 additions and 26 deletions
+43 -4
View File
@@ -76,20 +76,59 @@ class ProcessGuard:
self._restart_count = 3
return
log.error("主程序宕机,执行第 %d 次拉起:%s", attempts, self.config.main_start_cmd)
# 拉起方式:配置了服务名则走 nssm restart(幂等),否则回退启动命令
if self.config.main_service_name:
start_desc = f"nssm restart {self.config.main_service_name}"
else:
start_desc = self.config.main_start_cmd
log.error("主程序宕机,执行第 %d 次拉起:%s", attempts, start_desc)
self.client.send("ALERT", {
"level": "WARN",
"msg": f"主程序宕机,Agent 正在执行第 {attempts} 次拉起",
})
try:
proc = start_main_app(self.config.main_start_cmd)
log.info("启动命令已执行,pid=%s", proc.pid if proc else None)
if self.config.main_service_name:
ok, reason = restart_main_service(self.config.nssm_path, self.config.main_service_name)
if not ok:
log.error("nssm 拉起失败:%s", reason)
else:
proc = start_main_app(self.config.main_start_cmd)
log.info("启动命令已执行,pid=%s", proc.pid if proc else None)
except Exception as e:
log.error("启动命令执行失败:%s", e)
log.error("拉起失败:%s", e)
# ================= 公共工具(upgrade.py 复用) =================
def restart_main_service(nssm_path: str, service_name: str) -> tuple[bool, str]:
"""
通过 NSSM 重启主程序服务(幂等:无论服务处于停止/运行态均到运行态)。
nssm_path 为 nssm.exe 完整路径;service_name 为注册的 Windows 服务名。
返回 (是否成功, 输出/原因)。nssm 输出打印到日志,权限不足(拒绝访问)等
失败原因一眼可见。
"""
try:
proc = subprocess.run(
[nssm_path, "restart", service_name],
capture_output=True,
timeout=60,
)
# nssm 输出为 UTF-16 编码(宽字符),需按 utf-16 解码
out = ((proc.stdout or b"") + (proc.stderr or b"")).decode("utf-16", errors="replace").strip()
ok = proc.returncode == 0
if ok:
log.info("nssm restart %s 成功:%s", service_name, out or "(无输出)")
else:
log.warning("nssm restart %s 失败(exit=%s):%s", service_name, proc.returncode, out)
if "拒绝访问" in out or "Access" in out:
out += "(提示:重启服务需要管理员权限,请将 Agent 以 Windows 服务方式运行)"
return ok, out
except Exception as e:
log.warning("nssm restart %s 执行异常:%s", service_name, e)
return False, str(e)
def start_main_app(start_cmd: str) -> subprocess.Popen | None:
"""
拉起主程序。