| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- r"""服务化共用工作体 (2026-09-17 用户令 1: 观澜安装为服务)。
- Windows 服务 (`scripts/win_service.py`) 与 Linux systemd (`scripts/service_main.py`) 都调这里,
- **同一套起停/守护逻辑,只有外壳不同** —— 免得两平台各写一份、行为慢慢分叉。
- ## 它在做什么
- 起 : `guanlan.py serve`(幂等:已在跑的复用,缺的补起,等 healthz)
- 守护 : 每 `INTERVAL` 秒跑一次同样的 `guanlan.py serve` —— 哪个组件掉了就被拉起来;
- 同时看网关 `/healthz`,连续失败则记 ERROR(服务管理器据此判活)
- 停 : `guanlan.py stop`(连网关一起停;服务模式下整机停服就该全停)
- 为什么用**子进程 + 现成 CLI** 而不是把服务代码直接写进各组件:
- 服务管理器的语义是"一个前台进程代表整套系统",而观澜本来就是 6 个独立进程;
- 复用 `guanlan.py` 的两条命令,起停口径与手工运维**完全一致**,不会出现"服务起的和手工起的不一样"。
- """
- from __future__ import annotations
- import datetime as dt
- import pathlib
- import socket
- import subprocess
- import sys
- import time
- ROOT = pathlib.Path(__file__).resolve().parents[1]
- sys.path.insert(0, str(ROOT))
- from src import paths as P # noqa: E402
- from src import version as V # noqa: E402
- INTERVAL = 15 # 守护巡检间隔(秒)
- LOG = None # 由外壳设置为 logs/service.log
- def log(msg: str, level: str = 'INFO') -> None:
- """服务日志 —— 与 §11.2 同一行格式,落 `logs/<组件>.log` 的 service 一份。"""
- from src import logfile as _lf
- line = _lf.line(level, 'service', msg)
- print(line, flush=True)
- try:
- with open(_lf.component_log('service'), 'a', encoding='utf-8', newline='\n') as f:
- f.write(line + '\n')
- except Exception:
- pass
- def _py() -> str:
- return str(P.venv_python() or sys.executable)
- def run(cmd: list, timeout: int = 300) -> int:
- """跑一条 guanlan.py 子命令(捕获输出写进服务日志)。"""
- p = subprocess.run([_py(), 'guanlan.py'] + cmd, cwd=str(ROOT), capture_output=True, text=True,
- errors='replace', timeout=timeout)
- for ln in (p.stdout or '').splitlines():
- log(' ' + ln)
- for ln in (p.stderr or '').splitlines():
- log(' ' + ln, 'WARN')
- return p.returncode
- def gateway_up(host: str = '127.0.0.1') -> bool:
- try:
- with socket.create_connection((host, 28084), timeout=1.5):
- return True
- except OSError:
- return False
- def start() -> None:
- log(f'启动 {V.NAME} v{V.VERSION}({V.EDITION})')
- rc = run(['serve'])
- log(f'起组件完成 (rc={rc}; 1 = 有模块未就绪, 属正常)')
- def ensure() -> bool:
- """一次巡检:把缺的组件补起来。→ 网关是否在听。"""
- run(['serve'])
- return gateway_up()
- def stop() -> None:
- log('停止全部组件(含网关)')
- rc = run(['stop'])
- log(f'停止完成 (rc={rc})')
- def supervise(stop_evt, once: bool = False) -> None:
- """守护循环:直到 stop_evt 被置位(或 once=True 只跑一轮)。"""
- fails = 0
- while not stop_evt.is_set():
- try:
- ok = ensure()
- if not ok:
- fails += 1
- log(f'网关未就绪(连续 {fails} 次)', 'ERROR' if fails >= 3 else 'WARN')
- else:
- if fails:
- log('网关已恢复')
- fails = 0
- except Exception as e:
- log(f'巡检异常: {type(e).__name__}: {e}', 'ERROR')
- if once:
- return
- stop_evt.wait(INTERVAL)
- def selftest() -> int:
- """不需要任何权限的自证:依赖、端口、命令行是否齐备(服务注册前先看这个)。"""
- ok = True
- log(f'{V.NAME} v{V.VERSION} 服务化自检 @ {ROOT}')
- py = _py()
- log(f' 解释器: {py} (存在: {pathlib.Path(py).is_file()})')
- ok &= pathlib.Path(py).is_file()
- log(f' guanlan.py: {(ROOT / "guanlan.py").is_file()}')
- ok &= (ROOT / 'guanlan.py').is_file()
- log(f' 网关端口 28084 当前: ' + ('在听' if gateway_up() else '未监听'))
- log(f' 版本记录: {V.info_path(ROOT)}' + ('(已存在)' if V.info_path(ROOT).is_file() else '(无 —— 全新安装)'))
- # 组件端口占用检查(服务接管前这些端口应为空闲或已由本系统占用)
- for name, port in (('detail', 18033), ('cms', 18020), ('sim', 18791), ('sim_sys', 18792), ('viewer', 64292)):
- with socket.socket() as s:
- s.settimeout(0.3)
- busy = s.connect_ex(('127.0.0.1', port)) == 0
- log(f' 端口 {port} ({name}): ' + ('在听(多半是本系统已起)' if busy else '空闲'))
- log('自检结论: ' + ('服务注册前置条件齐备' if ok else '有缺项,见上'), 'INFO' if ok else 'ERROR')
- return 0 if ok else 1
|