- 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>
105 lines
3.1 KiB
Python
105 lines
3.1 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Agent 启动入口。
|
||
|
||
启动方式:
|
||
python -m agent.main (源码运行)
|
||
hospital-agent.exe (PyInstaller 打包后)
|
||
|
||
职责:
|
||
1. WebSocket 主动连接 B 端管理服务器(注册/心跳/命令)
|
||
2. 守护主程序(健康探测 + 宕机拉起)
|
||
|
||
由 WinSW/NSSM Windows 服务守护本进程。
|
||
"""
|
||
|
||
import logging
|
||
import logging.handlers
|
||
import os
|
||
import signal
|
||
import sys
|
||
import threading
|
||
|
||
from agent.config import AgentConfig
|
||
from agent.main_app_client import MainAppClient
|
||
|
||
log = logging.getLogger(__name__)
|
||
|
||
# 组装阶段先声明,供信号处理器引用
|
||
_dispatcher = None
|
||
_guard = None
|
||
_client = None
|
||
|
||
|
||
def run():
|
||
"""主流程:加载配置 → 组装组件 → 启动 → 主线程保活。"""
|
||
global _dispatcher, _guard, _client
|
||
|
||
config = AgentConfig.load()
|
||
_setup_logging(config.log_dir)
|
||
log.info("Agent 启动:deviceId=%s, server=%s", config.device_id, config.server_url)
|
||
# 源码运行时根目录即工作目录,PyInstaller 打包后同理(服务管理器负责设置工作目录)
|
||
# 组装组件(延迟导入避免环)
|
||
from agent.dispatcher import CommandDispatcher
|
||
from agent.process_guard import ProcessGuard
|
||
from agent.ws_client import AgentClient
|
||
|
||
main_app_client = MainAppClient(config)
|
||
dispatcher = CommandDispatcher(config, main_app_client)
|
||
client = AgentClient(config, dispatcher)
|
||
dispatcher.set_client(client)
|
||
guard = ProcessGuard(config, main_app_client, client)
|
||
_dispatcher, _guard, _client = dispatcher, guard, client
|
||
|
||
# 优雅停机信号处理
|
||
signal.signal(signal.SIGINT, _shutdown)
|
||
signal.signal(signal.SIGTERM, _shutdown)
|
||
|
||
client.start()
|
||
guard.start()
|
||
|
||
# 主线程保活
|
||
threading.Event().wait()
|
||
|
||
|
||
def _setup_logging(log_dir: str):
|
||
"""日志输出到控制台 + 文件(服务方式运行时靠文件留存,按大小轮转)。"""
|
||
fmt = logging.Formatter(
|
||
"{asctime} {levelname} [{name}] {message}",
|
||
style="{",
|
||
)
|
||
root = logging.getLogger()
|
||
root.setLevel(logging.INFO)
|
||
|
||
console = logging.StreamHandler()
|
||
console.setFormatter(fmt)
|
||
root.addHandler(console)
|
||
|
||
try:
|
||
os.makedirs(log_dir, exist_ok=True)
|
||
file_handler = logging.handlers.RotatingFileHandler(
|
||
os.path.join(log_dir, "agent.log"),
|
||
maxBytes=5 * 1024 * 1024, # 单文件 5MB
|
||
backupCount=5, # 保留 5 个轮转备份
|
||
encoding="utf-8",
|
||
)
|
||
file_handler.setFormatter(fmt)
|
||
root.addHandler(file_handler)
|
||
except Exception as e:
|
||
root.warning("日志文件初始化失败(仅控制台输出):%s", e)
|
||
|
||
|
||
def _shutdown(signum, frame):
|
||
"""优雅停机:释放日志流 → 停守护 → 断连接 → 退出。"""
|
||
log.info("Agent 停机中...(signal=%s)", signum)
|
||
if _dispatcher is not None:
|
||
_dispatcher.shutdown_log_stream()
|
||
if _guard is not None:
|
||
_guard.stop()
|
||
if _client is not None:
|
||
_client.stop()
|
||
sys.exit(0)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
run()
|