206 lines
7.7 KiB
Python
206 lines
7.7 KiB
Python
import subprocess
|
||
import sys
|
||
import threading
|
||
import atexit
|
||
import json
|
||
import os
|
||
import socket
|
||
import time
|
||
from pathlib import Path
|
||
from typing import Optional, Union, Dict, List
|
||
|
||
import psutil
|
||
|
||
from Utils.LogManager import LogManager
|
||
|
||
|
||
class FlaskSubprocessManager:
|
||
_instance: Optional['FlaskSubprocessManager'] = None
|
||
_lock: threading.Lock = threading.Lock()
|
||
|
||
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
|
||
self.comm_port = 34566
|
||
self._stop_event = threading.Event()
|
||
self._monitor_thread: Optional[threading.Thread] = None
|
||
# 新增:启动前先把可能残留的 Flask 干掉
|
||
self._kill_orphan_flask()
|
||
atexit.register(self.stop)
|
||
LogManager.info("FlaskSubprocessManager 单例已初始化", udid="system")
|
||
|
||
def _kill_orphan_flask(self):
|
||
"""根据端口 34566 把遗留进程全部杀掉"""
|
||
try:
|
||
if os.name == "nt":
|
||
# Windows
|
||
out = subprocess.check_output(
|
||
["netstat", "-ano"],
|
||
text=True, startupinfo=self._si
|
||
)
|
||
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)],
|
||
startupinfo=self._si,
|
||
capture_output=True)
|
||
else:
|
||
# macOS / Linux
|
||
out = subprocess.check_output(
|
||
["lsof", "-t", f"-iTCP:{self.comm_port}", "-sTCP:LISTEN"],
|
||
text=True
|
||
)
|
||
for pid in map(int, out.split()):
|
||
if pid != os.getpid():
|
||
os.kill(pid, 9)
|
||
except Exception:
|
||
pass
|
||
|
||
# ---------- 启动 ----------
|
||
def start(self):
|
||
with self._lock:
|
||
if self._is_alive():
|
||
LogManager.warning("子进程已在运行,无需重复启动", udid="system")
|
||
return
|
||
|
||
env = os.environ.copy()
|
||
env["FLASK_COMM_PORT"] = str(self.comm_port)
|
||
|
||
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:
|
||
cmd = [sys.executable, "-u", "-m", "Module.Main", "--role=flask"]
|
||
cwd = str(Path(__file__).resolve().parent)
|
||
|
||
LogManager.info(f"准备启动 Flask 子进程: {cmd} cwd={cwd}", udid="system")
|
||
|
||
# 关键:不再自己 open 文件,直接走 LogManager
|
||
# 用 PIPE 捕获,再转存到 system 级日志
|
||
self.process = subprocess.Popen(
|
||
cmd,
|
||
stdin=subprocess.DEVNULL,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.STDOUT,
|
||
text=True,
|
||
encoding="utf-8",
|
||
errors="replace",
|
||
bufsize=1,
|
||
env=env,
|
||
cwd=cwd,
|
||
start_new_session=True
|
||
)
|
||
|
||
# 守护线程:把子进程 stdout → LogManager.info/system
|
||
threading.Thread(target=self._flush_stdout, daemon=True).start()
|
||
|
||
LogManager.info(f"Flask 子进程已启动,PID={self.process.pid},端口={self.comm_port}", udid="system")
|
||
|
||
if not self._wait_port_open(timeout=10):
|
||
LogManager.error("等待端口监听超时,启动失败", udid="system")
|
||
self.stop()
|
||
raise RuntimeError("Flask 启动后 10 s 内未监听端口")
|
||
|
||
self._monitor_thread = threading.Thread(target=self._monitor, daemon=True)
|
||
self._monitor_thread.start()
|
||
LogManager.info("端口守护线程已启动", udid="system")
|
||
|
||
# ---------- 实时把子进程 stdout 刷到 system 日志 ----------
|
||
def _flush_stdout(self):
|
||
for line in iter(self.process.stdout.readline, ""):
|
||
if line:
|
||
LogManager.info(line.rstrip(), udid="system")
|
||
self.process.stdout.close()
|
||
|
||
# ---------- 发送 ----------
|
||
def send(self, data: Union[str, Dict, List]) -> bool:
|
||
if isinstance(data, (dict, list)):
|
||
data = json.dumps(data, ensure_ascii=False)
|
||
try:
|
||
with socket.create_connection(("127.0.0.1", self.comm_port), timeout=3.0) as s:
|
||
s.sendall((data + "\n").encode("utf-8"))
|
||
LogManager.info(f"数据已成功发送到 Flask 端口:{self.comm_port}", udid="system")
|
||
return True
|
||
except Exception as e:
|
||
LogManager.error(f"发送失败:{e}", udid="system")
|
||
return False
|
||
|
||
# ---------- 停止 ----------
|
||
def stop(self):
|
||
with self._lock:
|
||
if not self.process:
|
||
return
|
||
pid = self.process.pid
|
||
LogManager.info(f"正在停止 Flask 子进程 PID={pid}", udid="system")
|
||
try:
|
||
# 1. 杀整棵树(Windows 也适用)
|
||
parent = psutil.Process(pid)
|
||
for child in parent.children(recursive=True):
|
||
child.kill()
|
||
parent.kill()
|
||
gone, alive = psutil.wait_procs([parent] + parent.children(), timeout=3)
|
||
for p in alive:
|
||
p.kill() # 保险再补一刀
|
||
self.process.wait()
|
||
except psutil.NoSuchProcess:
|
||
pass
|
||
except Exception as e:
|
||
LogManager.error(f"停止子进程异常:{e}", udid="system")
|
||
finally:
|
||
self.process = None
|
||
self._stop_event.set()
|
||
|
||
# ---------- 端口守护 ----------
|
||
def _monitor(self):
|
||
LogManager.info("守护线程开始运行,周期性检查端口存活", udid="system")
|
||
while not self._stop_event.wait(1.0):
|
||
if not self._port_alive():
|
||
LogManager.error("检测到端口不通,准备重启 Flask", udid="system")
|
||
with self._lock:
|
||
if self.process and self.process.poll() is None:
|
||
self.stop()
|
||
try:
|
||
self.start()
|
||
except Exception as e:
|
||
LogManager.error(f"自动重启失败:{e}", udid="system")
|
||
time.sleep(2)
|
||
|
||
# ---------- 辅助 ----------
|
||
def _is_port_busy(self, port: int) -> bool:
|
||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||
s.settimeout(0.2)
|
||
return s.connect_ex(("127.0.0.1", port)) == 0
|
||
|
||
def _port_alive(self) -> bool:
|
||
try:
|
||
with socket.create_connection(("127.0.0.1", self.comm_port), timeout=0.5):
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
def _wait_port_open(self, timeout: float) -> bool:
|
||
t0 = time.time()
|
||
while time.time() - t0 < timeout:
|
||
if self._port_alive():
|
||
return True
|
||
time.sleep(0.2)
|
||
return False
|
||
|
||
def _is_alive(self) -> bool:
|
||
return self.process is not None and self.process.poll() is None and self._port_alive()
|
||
|
||
@classmethod
|
||
def get_instance(cls) -> 'FlaskSubprocessManager':
|
||
return cls() |