2025-09-17 22:23:57 +08:00
|
|
|
|
import os
|
|
|
|
|
|
import signal
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import threading
|
|
|
|
|
|
import time
|
|
|
|
|
|
import psutil
|
|
|
|
|
|
import subprocess
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from threading import Event, Thread
|
|
|
|
|
|
from typing import Dict, Optional
|
2025-08-06 22:11:33 +08:00
|
|
|
|
|
|
|
|
|
|
from Utils.LogManager import LogManager
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-09-17 22:23:57 +08:00
|
|
|
|
class ThreadManager:
|
|
|
|
|
|
"""
|
|
|
|
|
|
对调用方完全透明:
|
|
|
|
|
|
add(udid, thread_obj, stop_event) 保持原签名
|
|
|
|
|
|
stop(udid) 保持原签名
|
|
|
|
|
|
但内部把 thread_obj 当成“壳”,真正拉起的是子进程。
|
|
|
|
|
|
"""
|
|
|
|
|
|
_pool: Dict[str, psutil.Process] = {}
|
|
|
|
|
|
_lock = threading.Lock()
|
2025-08-06 22:11:33 +08:00
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2025-09-17 22:23:57 +08:00
|
|
|
|
def add(cls, udid: str, dummy_thread, dummy_event: Event) -> None:
|
|
|
|
|
|
LogManager.method_info(f"【1】入口 udid={udid} 长度={len(udid)}", method="task")
|
|
|
|
|
|
if udid in cls._pool:
|
|
|
|
|
|
LogManager.method_warning(f"{udid} 仍在运行,先强制清理旧任务", method="task")
|
|
|
|
|
|
cls.stop(udid)
|
|
|
|
|
|
LogManager.method_info(f"【2】判断旧任务后 udid={udid} 长度={len(udid)}", method="task")
|
|
|
|
|
|
port = cls._find_free_port()
|
|
|
|
|
|
LogManager.method_info(f"【3】找端口后 udid={udid} 长度={len(udid)}", method="task")
|
|
|
|
|
|
proc = cls._start_worker_process(udid, port)
|
|
|
|
|
|
LogManager.method_info(f"【4】子进程启动后 udid={udid} 长度={len(udid)}", method="task")
|
|
|
|
|
|
cls._pool[udid] = proc
|
|
|
|
|
|
LogManager.method_info(f"【5】已写入字典,udid={udid} 长度={len(udid)}", method="task")
|
2025-08-06 22:11:33 +08:00
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2025-09-17 22:23:57 +08:00
|
|
|
|
def stop(cls, udid: str) -> tuple[int, str]:
|
|
|
|
|
|
with cls._lock: # 类级锁
|
|
|
|
|
|
proc = cls._pool.get(udid) # 1. 只读,不删
|
|
|
|
|
|
if proc is None:
|
|
|
|
|
|
return 1001, f"无此任务 {udid}"
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
proc.terminate()
|
|
|
|
|
|
gone, alive = psutil.wait_procs([proc], timeout=3)
|
|
|
|
|
|
if alive:
|
|
|
|
|
|
for p in alive:
|
|
|
|
|
|
for child in p.children(recursive=True):
|
|
|
|
|
|
child.kill()
|
|
|
|
|
|
p.kill()
|
|
|
|
|
|
psutil.wait_procs(alive, timeout=2)
|
|
|
|
|
|
|
|
|
|
|
|
# 正常退出
|
|
|
|
|
|
cls._pool.pop(udid)
|
|
|
|
|
|
LogManager.method_info("任务停止成功", method="task")
|
|
|
|
|
|
return 200, f"停止线程成功 {udid}"
|
|
|
|
|
|
|
|
|
|
|
|
except psutil.NoSuchProcess: # 精准捕获
|
|
|
|
|
|
cls._pool.pop(udid)
|
|
|
|
|
|
LogManager.method_info("进程已自然退出", method="task")
|
|
|
|
|
|
return 200, f"进程已退出 {udid}"
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e: # 真正的异常
|
|
|
|
|
|
LogManager.method_error(f"停止异常: {e}", method="task")
|
|
|
|
|
|
return 1002, f"停止异常 {udid}"
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------
|
|
|
|
|
|
# 以下全是内部工具,外部无需调用
|
|
|
|
|
|
# ------------------------------------------------------
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _find_free_port(start: int = 50000) -> int:
|
|
|
|
|
|
"""找个随机空闲端口,给子进程当通信口(可选)"""
|
|
|
|
|
|
import socket
|
|
|
|
|
|
for p in range(start, start + 1000):
|
|
|
|
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
|
|
|
|
if s.connect_ex(("127.0.0.1", p)) != 0:
|
|
|
|
|
|
return p
|
|
|
|
|
|
raise RuntimeError("无可用端口")
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _start_worker_process(udid: str, port: int) -> psutil.Process:
|
|
|
|
|
|
"""
|
|
|
|
|
|
真正拉起子进程:
|
|
|
|
|
|
打包环境:exe --udid=xxx
|
|
|
|
|
|
源码环境:python -m Module.Worker --udid=xxx
|
|
|
|
|
|
"""
|
|
|
|
|
|
exe_path = Path(sys.executable).resolve()
|
|
|
|
|
|
is_frozen = exe_path.suffix.lower() == ".exe" and exe_path.exists()
|
|
|
|
|
|
|
|
|
|
|
|
if is_frozen:
|
|
|
|
|
|
# 打包后
|
|
|
|
|
|
cmd = [str(exe_path), "--role=worker", f"--udid={udid}", f"--port={port}"]
|
|
|
|
|
|
cwd = str(exe_path.parent)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 源码运行
|
|
|
|
|
|
cmd = [sys.executable, "-u", "-m", "Module.Worker", f"--udid={udid}", f"--port={port}"]
|
|
|
|
|
|
cwd = str(Path(__file__).resolve().parent.parent)
|
|
|
|
|
|
|
|
|
|
|
|
# 核心:CREATE_NO_WINDOW + 独立会话,父进程死也不影响
|
|
|
|
|
|
creation_flags = 0x08000000 if os.name == "nt" else 0
|
|
|
|
|
|
proc = subprocess.Popen(
|
|
|
|
|
|
cmd,
|
|
|
|
|
|
stdin=subprocess.DEVNULL,
|
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
|
stderr=subprocess.STDOUT,
|
|
|
|
|
|
text=True,
|
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
|
errors="replace",
|
|
|
|
|
|
bufsize=1,
|
|
|
|
|
|
cwd=cwd,
|
|
|
|
|
|
start_new_session=True, # 独立进程组
|
|
|
|
|
|
creationflags=creation_flags
|
|
|
|
|
|
)
|
|
|
|
|
|
# 守护线程:把子进程 stdout 实时打到日志
|
|
|
|
|
|
Thread(target=lambda: ThreadManager._log_stdout(proc, udid), daemon=True).start()
|
|
|
|
|
|
return psutil.Process(proc.pid)
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _log_stdout(proc: subprocess.Popen, udid: str):
|
|
|
|
|
|
for line in iter(proc.stdout.readline, ""):
|
|
|
|
|
|
if line:
|
|
|
|
|
|
LogManager.info(line.rstrip(), udid)
|
|
|
|
|
|
proc.stdout.close()
|