2025-11-05 17:07:51 +08:00
|
|
|
|
# -*- coding: utf-8 -*-
|
2025-08-01 13:43:51 +08:00
|
|
|
|
import subprocess
|
2025-08-15 20:04:59 +08:00
|
|
|
|
import sys
|
2025-08-01 13:43:51 +08:00
|
|
|
|
import threading
|
|
|
|
|
|
import atexit
|
|
|
|
|
|
import json
|
|
|
|
|
|
import os
|
|
|
|
|
|
import socket
|
|
|
|
|
|
import time
|
2025-08-15 20:04:59 +08:00
|
|
|
|
from pathlib import Path
|
2025-08-01 13:43:51 +08:00
|
|
|
|
from typing import Optional, Union, Dict, List
|
|
|
|
|
|
|
2025-09-17 22:23:57 +08:00
|
|
|
|
import psutil
|
2025-08-20 13:48:32 +08:00
|
|
|
|
from Utils.LogManager import LogManager
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-08-01 13:43:51 +08:00
|
|
|
|
class FlaskSubprocessManager:
|
2025-11-05 17:07:51 +08:00
|
|
|
|
"""Flask 子进程守护 + 看门狗 + 稳定增强"""
|
|
|
|
|
|
|
2025-08-01 13:43:51 +08:00
|
|
|
|
_instance: Optional['FlaskSubprocessManager'] = None
|
2025-11-05 17:07:51 +08:00
|
|
|
|
_lock = threading.Lock()
|
2025-08-01 13:43:51 +08:00
|
|
|
|
|
|
|
|
|
|
def __new__(cls):
|
|
|
|
|
|
with cls._lock:
|
|
|
|
|
|
if cls._instance is None:
|
|
|
|
|
|
cls._instance = super().__new__(cls)
|
|
|
|
|
|
cls._instance._init_manager()
|
|
|
|
|
|
return cls._instance
|
|
|
|
|
|
|
|
|
|
|
|
def _init_manager(self):
|
|
|
|
|
|
self.process: Optional[subprocess.Popen] = None
|
2025-08-20 13:48:32 +08:00
|
|
|
|
self.comm_port = 34566
|
2025-08-01 13:43:51 +08:00
|
|
|
|
self._stop_event = threading.Event()
|
2025-09-15 16:01:27 +08:00
|
|
|
|
self._monitor_thread: Optional[threading.Thread] = None
|
2025-11-05 17:07:51 +08:00
|
|
|
|
|
|
|
|
|
|
# 看门狗参数
|
|
|
|
|
|
self._FAIL_THRESHOLD = int(os.getenv("FLASK_WD_FAIL_THRESHOLD", "3")) # 连续失败多少次重启
|
|
|
|
|
|
self._COOLDOWN_SEC = float(os.getenv("FLASK_WD_COOLDOWN", "8.0")) # 两次重启间隔
|
|
|
|
|
|
self._MAX_RESTARTS = int(os.getenv("FLASK_WD_MAX_RESTARTS", "5")) # 10分钟最多几次重启
|
|
|
|
|
|
self._RESTART_WINDOW = 600 # 10分钟
|
|
|
|
|
|
self._restart_times: List[float] = []
|
|
|
|
|
|
self._fail_count = 0
|
|
|
|
|
|
self._last_restart_time = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
# Windows 隐藏子窗口启动参数
|
|
|
|
|
|
self._si = None
|
|
|
|
|
|
if os.name == "nt":
|
|
|
|
|
|
si = subprocess.STARTUPINFO()
|
|
|
|
|
|
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
|
|
|
|
si.wShowWindow = 0
|
|
|
|
|
|
self._si = si
|
|
|
|
|
|
|
2025-09-17 22:23:57 +08:00
|
|
|
|
self._kill_orphan_flask()
|
2025-08-01 13:43:51 +08:00
|
|
|
|
atexit.register(self.stop)
|
2025-11-05 17:07:51 +08:00
|
|
|
|
self._log("info", "FlaskSubprocessManager 初始化完成")
|
|
|
|
|
|
|
|
|
|
|
|
# ========= 日志工具 =========
|
|
|
|
|
|
def _log(self, level: str, msg: str, udid="system"):
|
|
|
|
|
|
"""同时写 LogManager + 控制台"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if level == "info":
|
|
|
|
|
|
LogManager.info(msg, udid=udid)
|
|
|
|
|
|
elif level in ("warn", "warning"):
|
|
|
|
|
|
LogManager.warning(msg, udid=udid)
|
|
|
|
|
|
elif level == "error":
|
|
|
|
|
|
LogManager.error(msg, udid=udid)
|
|
|
|
|
|
else:
|
|
|
|
|
|
LogManager.info(msg, udid=udid)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
print(msg)
|
2025-08-01 13:43:51 +08:00
|
|
|
|
|
2025-11-05 17:07:51 +08:00
|
|
|
|
# ========= 杀残留 Flask =========
|
2025-09-17 22:23:57 +08:00
|
|
|
|
def _kill_orphan_flask(self):
|
|
|
|
|
|
try:
|
|
|
|
|
|
if os.name == "nt":
|
2025-11-05 17:07:51 +08:00
|
|
|
|
out = subprocess.check_output(["netstat", "-ano"], text=True, startupinfo=self._si)
|
2025-09-17 22:23:57 +08:00
|
|
|
|
for line in out.splitlines():
|
|
|
|
|
|
if f"127.0.0.1:{self.comm_port}" in line and "LISTENING" in line:
|
|
|
|
|
|
pid = int(line.strip().split()[-1])
|
|
|
|
|
|
if pid != os.getpid():
|
|
|
|
|
|
subprocess.run(["taskkill", "/F", "/PID", str(pid)],
|
2025-11-05 17:07:51 +08:00
|
|
|
|
startupinfo=self._si, capture_output=True)
|
|
|
|
|
|
self._log("warn", f"[FlaskMgr] 杀死残留进程 PID={pid}")
|
2025-09-17 22:23:57 +08:00
|
|
|
|
else:
|
2025-11-05 17:07:51 +08:00
|
|
|
|
out = subprocess.check_output(["lsof", "-t", f"-iTCP:{self.comm_port}", "-sTCP:LISTEN"], text=True)
|
2025-09-17 22:23:57 +08:00
|
|
|
|
for pid in map(int, out.split()):
|
|
|
|
|
|
if pid != os.getpid():
|
|
|
|
|
|
os.kill(pid, 9)
|
2025-11-05 17:07:51 +08:00
|
|
|
|
self._log("warn", f"[FlaskMgr] 杀死残留进程 PID={pid}")
|
2025-09-17 22:23:57 +08:00
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2025-11-05 17:07:51 +08:00
|
|
|
|
# ========= 启动 =========
|
2025-08-01 13:43:51 +08:00
|
|
|
|
def start(self):
|
|
|
|
|
|
with self._lock:
|
2025-09-15 16:01:27 +08:00
|
|
|
|
if self._is_alive():
|
2025-11-05 17:07:51 +08:00
|
|
|
|
self._log("warn", "[FlaskMgr] 子进程已在运行,无需重复启动")
|
2025-09-15 16:01:27 +08:00
|
|
|
|
return
|
2025-08-15 20:04:59 +08:00
|
|
|
|
|
2025-08-01 13:43:51 +08:00
|
|
|
|
env = os.environ.copy()
|
2025-08-15 20:04:59 +08:00
|
|
|
|
env["FLASK_COMM_PORT"] = str(self.comm_port)
|
2025-11-05 17:07:51 +08:00
|
|
|
|
|
2025-08-15 20:04:59 +08:00
|
|
|
|
exe_path = Path(sys.executable).resolve()
|
|
|
|
|
|
if exe_path.name.lower() in ("python.exe", "pythonw.exe"):
|
|
|
|
|
|
exe_path = Path(sys.argv[0]).resolve()
|
|
|
|
|
|
is_frozen = exe_path.suffix.lower() == ".exe" and exe_path.exists()
|
|
|
|
|
|
|
|
|
|
|
|
if is_frozen:
|
|
|
|
|
|
cmd = [str(exe_path), "--role=flask"]
|
|
|
|
|
|
cwd = str(exe_path.parent)
|
|
|
|
|
|
else:
|
2025-11-05 17:07:51 +08:00
|
|
|
|
project_root = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
candidates = [
|
|
|
|
|
|
project_root / "Module" / "Main.py",
|
|
|
|
|
|
project_root / "Main.py",
|
|
|
|
|
|
]
|
|
|
|
|
|
main_path = next((p for p in candidates if p.is_file()), None)
|
|
|
|
|
|
if main_path:
|
|
|
|
|
|
cmd = [sys.executable, "-u", str(main_path), "--role=flask"]
|
|
|
|
|
|
else:
|
|
|
|
|
|
cmd = [sys.executable, "-u", "-m", "Module.Main", "--role=flask"]
|
|
|
|
|
|
cwd = str(project_root)
|
2025-08-15 20:04:59 +08:00
|
|
|
|
|
2025-11-05 17:07:51 +08:00
|
|
|
|
self._log("info", f"[FlaskMgr] 启动命令: {cmd}, cwd={cwd}")
|
2025-08-01 13:43:51 +08:00
|
|
|
|
|
|
|
|
|
|
self.process = subprocess.Popen(
|
2025-08-15 20:04:59 +08:00
|
|
|
|
cmd,
|
2025-09-15 16:01:27 +08:00
|
|
|
|
stdin=subprocess.DEVNULL,
|
2025-08-15 20:04:59 +08:00
|
|
|
|
stdout=subprocess.PIPE,
|
2025-09-15 16:01:27 +08:00
|
|
|
|
stderr=subprocess.STDOUT,
|
2025-08-15 20:04:59 +08:00
|
|
|
|
text=True,
|
|
|
|
|
|
encoding="utf-8",
|
2025-09-15 16:01:27 +08:00
|
|
|
|
errors="replace",
|
2025-08-15 20:04:59 +08:00
|
|
|
|
bufsize=1,
|
|
|
|
|
|
env=env,
|
|
|
|
|
|
cwd=cwd,
|
2025-11-05 17:07:51 +08:00
|
|
|
|
start_new_session=True,
|
|
|
|
|
|
startupinfo=self._si
|
2025-08-01 13:43:51 +08:00
|
|
|
|
)
|
2025-08-20 13:48:32 +08:00
|
|
|
|
|
2025-09-15 16:01:27 +08:00
|
|
|
|
threading.Thread(target=self._flush_stdout, daemon=True).start()
|
2025-11-05 17:07:51 +08:00
|
|
|
|
self._log("info", f"[FlaskMgr] Flask 子进程已启动,PID={self.process.pid}")
|
2025-08-01 13:43:51 +08:00
|
|
|
|
|
2025-09-15 16:01:27 +08:00
|
|
|
|
if not self._wait_port_open(timeout=10):
|
2025-11-05 17:07:51 +08:00
|
|
|
|
self._log("error", "[FlaskMgr] 启动失败,端口未监听")
|
2025-09-15 16:01:27 +08:00
|
|
|
|
self.stop()
|
2025-11-05 17:07:51 +08:00
|
|
|
|
raise RuntimeError("Flask 启动后 10s 内未监听端口")
|
2025-08-01 13:43:51 +08:00
|
|
|
|
|
2025-11-05 17:07:51 +08:00
|
|
|
|
if not self._monitor_thread or not self._monitor_thread.is_alive():
|
|
|
|
|
|
self._monitor_thread = threading.Thread(target=self._monitor, daemon=True)
|
|
|
|
|
|
self._monitor_thread.start()
|
|
|
|
|
|
self._log("info", "[FlaskWD] 守护线程已启动")
|
2025-09-15 16:01:27 +08:00
|
|
|
|
|
2025-11-05 17:07:51 +08:00
|
|
|
|
# ========= stdout捕获 =========
|
2025-09-15 16:01:27 +08:00
|
|
|
|
def _flush_stdout(self):
|
2025-11-05 17:07:51 +08:00
|
|
|
|
if not self.process or not self.process.stdout:
|
|
|
|
|
|
return
|
2025-09-15 16:01:27 +08:00
|
|
|
|
for line in iter(self.process.stdout.readline, ""):
|
|
|
|
|
|
if line:
|
2025-11-05 17:07:51 +08:00
|
|
|
|
self._log("info", line.rstrip())
|
2025-09-15 16:01:27 +08:00
|
|
|
|
self.process.stdout.close()
|
|
|
|
|
|
|
2025-11-05 17:07:51 +08:00
|
|
|
|
# ========= 发送 =========
|
2025-08-01 13:43:51 +08:00
|
|
|
|
def send(self, data: Union[str, Dict, List]) -> bool:
|
2025-09-15 16:01:27 +08:00
|
|
|
|
if isinstance(data, (dict, list)):
|
|
|
|
|
|
data = json.dumps(data, ensure_ascii=False)
|
2025-08-01 13:43:51 +08:00
|
|
|
|
try:
|
2025-09-15 16:01:27 +08:00
|
|
|
|
with socket.create_connection(("127.0.0.1", self.comm_port), timeout=3.0) as s:
|
|
|
|
|
|
s.sendall((data + "\n").encode("utf-8"))
|
2025-11-05 17:07:51 +08:00
|
|
|
|
self._log("info", f"[FlaskMgr] 数据已发送到端口 {self.comm_port}")
|
|
|
|
|
|
return True
|
2025-08-01 13:43:51 +08:00
|
|
|
|
except Exception as e:
|
2025-11-05 17:07:51 +08:00
|
|
|
|
self._log("error", f"[FlaskMgr] 发送失败: {e}")
|
2025-08-01 13:43:51 +08:00
|
|
|
|
return False
|
|
|
|
|
|
|
2025-11-05 17:07:51 +08:00
|
|
|
|
# ========= 停止 =========
|
2025-08-01 13:43:51 +08:00
|
|
|
|
def stop(self):
|
|
|
|
|
|
with self._lock:
|
2025-09-17 22:23:57 +08:00
|
|
|
|
if not self.process:
|
2025-09-15 16:01:27 +08:00
|
|
|
|
return
|
|
|
|
|
|
pid = self.process.pid
|
2025-11-05 17:07:51 +08:00
|
|
|
|
self._log("info", f"[FlaskMgr] 正在停止子进程 PID={pid}")
|
2025-09-15 16:01:27 +08:00
|
|
|
|
try:
|
2025-09-17 22:23:57 +08:00
|
|
|
|
parent = psutil.Process(pid)
|
|
|
|
|
|
for child in parent.children(recursive=True):
|
2025-11-05 17:07:51 +08:00
|
|
|
|
try:
|
|
|
|
|
|
child.kill()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
2025-09-17 22:23:57 +08:00
|
|
|
|
parent.kill()
|
2025-11-05 17:07:51 +08:00
|
|
|
|
parent.wait(timeout=3)
|
2025-09-17 22:23:57 +08:00
|
|
|
|
except psutil.NoSuchProcess:
|
|
|
|
|
|
pass
|
2025-09-15 16:01:27 +08:00
|
|
|
|
except Exception as e:
|
2025-11-05 17:07:51 +08:00
|
|
|
|
self._log("error", f"[FlaskMgr] 停止子进程异常: {e}")
|
2025-09-15 16:01:27 +08:00
|
|
|
|
finally:
|
|
|
|
|
|
self.process = None
|
2025-08-01 13:43:51 +08:00
|
|
|
|
self._stop_event.set()
|
2025-09-15 16:01:27 +08:00
|
|
|
|
|
2025-11-05 17:07:51 +08:00
|
|
|
|
# ========= 看门狗 =========
|
2025-09-15 16:01:27 +08:00
|
|
|
|
def _monitor(self):
|
2025-11-05 17:07:51 +08:00
|
|
|
|
self._log("info", "[FlaskWD] 看门狗线程启动")
|
|
|
|
|
|
verbose = os.getenv("FLASK_WD_VERBOSE", "0") == "1"
|
|
|
|
|
|
last_ok = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
while not self._stop_event.wait(2.0):
|
|
|
|
|
|
alive = self._port_alive()
|
|
|
|
|
|
if alive:
|
|
|
|
|
|
self._fail_count = 0
|
|
|
|
|
|
if verbose and (time.time() - last_ok) >= 60:
|
|
|
|
|
|
self._log("info", f"[FlaskWD] OK {self.comm_port} alive")
|
|
|
|
|
|
last_ok = time.time()
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
self._fail_count += 1
|
|
|
|
|
|
self._log("warn", f"[FlaskWD] 探测失败 {self._fail_count}/{self._FAIL_THRESHOLD}")
|
|
|
|
|
|
|
|
|
|
|
|
if self._fail_count >= self._FAIL_THRESHOLD:
|
|
|
|
|
|
now = time.time()
|
|
|
|
|
|
if now - self._last_restart_time < self._COOLDOWN_SEC:
|
|
|
|
|
|
self._log("warn", "[FlaskWD] 冷却中,跳过重启")
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 限速:10分钟内超过MAX_RESTARTS则不再重启
|
|
|
|
|
|
self._restart_times = [t for t in self._restart_times if now - t < self._RESTART_WINDOW]
|
|
|
|
|
|
if len(self._restart_times) >= self._MAX_RESTARTS:
|
|
|
|
|
|
self._log("error", f"[FlaskWD] 10分钟内重启次数过多({len(self._restart_times)}次),暂停看门狗")
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
self._restart_times.append(now)
|
|
|
|
|
|
self._log("warn", "[FlaskWD] 端口不通,准备重启 Flask")
|
|
|
|
|
|
|
2025-09-15 16:01:27 +08:00
|
|
|
|
with self._lock:
|
2025-11-05 17:07:51 +08:00
|
|
|
|
try:
|
2025-09-15 16:01:27 +08:00
|
|
|
|
self.stop()
|
2025-11-05 17:07:51 +08:00
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
self.start()
|
|
|
|
|
|
self._fail_count = 0
|
|
|
|
|
|
self._last_restart_time = now
|
|
|
|
|
|
self._log("info", "[FlaskWD] Flask 已成功重启")
|
|
|
|
|
|
from Module.DeviceInfo import DeviceInfo
|
|
|
|
|
|
info = DeviceInfo()
|
|
|
|
|
|
with info._lock:
|
|
|
|
|
|
for m in info._models.values():
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.send(m.toDict())
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
self._log("error", f"[FlaskWD] 自动重启失败: {e}")
|
|
|
|
|
|
time.sleep(3)
|
2025-09-15 16:01:27 +08:00
|
|
|
|
|
2025-11-05 17:07:51 +08:00
|
|
|
|
# ========= 辅助 =========
|
2025-09-15 16:01:27 +08:00
|
|
|
|
def _port_alive(self) -> bool:
|
|
|
|
|
|
try:
|
2025-11-05 17:07:51 +08:00
|
|
|
|
with socket.create_connection(("127.0.0.1", self.comm_port), timeout=0.6):
|
2025-09-15 16:01:27 +08:00
|
|
|
|
return True
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def _wait_port_open(self, timeout: float) -> bool:
|
2025-11-05 17:07:51 +08:00
|
|
|
|
start = time.time()
|
|
|
|
|
|
while time.time() - start < timeout:
|
2025-09-15 16:01:27 +08:00
|
|
|
|
if self._port_alive():
|
|
|
|
|
|
return True
|
|
|
|
|
|
time.sleep(0.2)
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def _is_alive(self) -> bool:
|
2025-11-05 17:07:51 +08:00
|
|
|
|
return self.process and self.process.poll() is None and self._port_alive()
|
2025-08-01 13:43:51 +08:00
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def get_instance(cls) -> 'FlaskSubprocessManager':
|
|
|
|
|
|
return cls()
|