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:
|
|
|
|
|
|
_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
|
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-09-17 22:23:57 +08:00
|
|
|
|
# 新增:启动前先把可能残留的 Flask 干掉
|
|
|
|
|
|
self._kill_orphan_flask()
|
2025-08-01 13:43:51 +08:00
|
|
|
|
atexit.register(self.stop)
|
2025-09-15 16:01:27 +08:00
|
|
|
|
LogManager.info("FlaskSubprocessManager 单例已初始化", udid="system")
|
2025-08-01 13:43:51 +08:00
|
|
|
|
|
2025-09-17 22:23:57 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2025-09-15 16:01:27 +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():
|
|
|
|
|
|
LogManager.warning("子进程已在运行,无需重复启动", udid="system")
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
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-09-15 16:01:27 +08:00
|
|
|
|
cmd = [sys.executable, "-u", "-m", "Module.Main", "--role=flask"]
|
|
|
|
|
|
cwd = str(Path(__file__).resolve().parent)
|
2025-08-15 20:04:59 +08:00
|
|
|
|
|
2025-09-15 16:01:27 +08:00
|
|
|
|
LogManager.info(f"准备启动 Flask 子进程: {cmd} cwd={cwd}", udid="system")
|
2025-08-01 13:43:51 +08:00
|
|
|
|
|
2025-09-15 16:01:27 +08:00
|
|
|
|
# 关键:不再自己 open 文件,直接走 LogManager
|
|
|
|
|
|
# 用 PIPE 捕获,再转存到 system 级日志
|
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-09-15 16:01:27 +08:00
|
|
|
|
start_new_session=True
|
2025-08-01 13:43:51 +08:00
|
|
|
|
)
|
2025-08-20 13:48:32 +08:00
|
|
|
|
|
2025-09-15 16:01:27 +08:00
|
|
|
|
# 守护线程:把子进程 stdout → LogManager.info/system
|
|
|
|
|
|
threading.Thread(target=self._flush_stdout, daemon=True).start()
|
2025-08-01 13:43:51 +08:00
|
|
|
|
|
2025-09-15 16:01:27 +08:00
|
|
|
|
LogManager.info(f"Flask 子进程已启动,PID={self.process.pid},端口={self.comm_port}", udid="system")
|
2025-08-01 13:43:51 +08:00
|
|
|
|
|
2025-09-15 16:01:27 +08:00
|
|
|
|
if not self._wait_port_open(timeout=10):
|
|
|
|
|
|
LogManager.error("等待端口监听超时,启动失败", udid="system")
|
|
|
|
|
|
self.stop()
|
|
|
|
|
|
raise RuntimeError("Flask 启动后 10 s 内未监听端口")
|
2025-08-01 13:43:51 +08:00
|
|
|
|
|
2025-09-15 16:01:27 +08:00
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
# ---------- 发送 ----------
|
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"))
|
|
|
|
|
|
LogManager.info(f"数据已成功发送到 Flask 端口:{self.comm_port}", udid="system")
|
|
|
|
|
|
return True
|
2025-08-01 13:43:51 +08:00
|
|
|
|
except Exception as e:
|
2025-09-15 16:01:27 +08:00
|
|
|
|
LogManager.error(f"发送失败:{e}", udid="system")
|
2025-08-01 13:43:51 +08:00
|
|
|
|
return False
|
|
|
|
|
|
|
2025-09-15 16:01:27 +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
|
|
|
|
|
|
LogManager.info(f"正在停止 Flask 子进程 PID={pid}", udid="system")
|
|
|
|
|
|
try:
|
2025-09-17 22:23:57 +08:00
|
|
|
|
# 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
|
2025-09-15 16:01:27 +08:00
|
|
|
|
except Exception as e:
|
2025-09-17 22:23:57 +08:00
|
|
|
|
LogManager.error(f"停止子进程异常:{e}", udid="system")
|
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
|
|
|
|
|
|
|
|
|
|
# ---------- 端口守护 ----------
|
|
|
|
|
|
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()
|
2025-08-01 13:43:51 +08:00
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def get_instance(cls) -> 'FlaskSubprocessManager':
|
|
|
|
|
|
return cls()
|