优化停止任务逻辑

This commit is contained in:
2025-09-18 20:09:52 +08:00
parent 9250be8780
commit c54a0aceb5
24 changed files with 87 additions and 165 deletions

9
.idea/workspace.xml generated
View File

@@ -5,9 +5,18 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成"> <list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成">
<change afterPath="$PROJECT_DIR$/resources/133bffc17635b7a3bd709492b7a519d96710a4a2/bgv.png" afterDir="false" />
<change afterPath="$PROJECT_DIR$/resources/3157d8bd35c230012cb3640d2f26ffd0b61a10d1/bgv.png" afterDir="false" />
<change afterPath="$PROJECT_DIR$/resources/f2fb0c352df29415f69708d90a9daba702ae4917/bgv.png" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/iOSAI.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/iOSAI.iml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/iOSAI.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/iOSAI.iml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Entity/ResultData.py" beforeDir="false" afterPath="$PROJECT_DIR$/Entity/ResultData.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Module/FlaskService.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/FlaskService.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Module/FlaskSubprocessManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/FlaskSubprocessManager.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Module/Main.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/Main.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Utils/LogManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/LogManager.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Utils/ThreadManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/ThreadManager.py" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />

View File

@@ -2,15 +2,15 @@ import json
# 返回数据模型 # 返回数据模型
class ResultData(object): class ResultData(object):
def __init__(self, code=200, data=None, massage="获取成功"): def __init__(self, code=200, data=None, message="获取成功"):
super(ResultData, self).__init__() super(ResultData, self).__init__()
self.code = code self.code = code
self.data = data self.data = data
self.massage = massage self.message = message
def toJson(self): def toJson(self):
return json.dumps({ return json.dumps({
"code": self.code, "code": self.code,
"data": self.data, "data": self.data,
"massage": self.massage "message": self.message
}, ensure_ascii=False) # ensure_ascii=False 确保中文不会被转义 }, ensure_ascii=False) # ensure_ascii=False 确保中文不会被转义

View File

@@ -243,8 +243,8 @@ def growAccount():
thread = threading.Thread(target=manager.growAccount, args=(udid, event)) thread = threading.Thread(target=manager.growAccount, args=(udid, event))
thread.start() thread.start()
# 添加到线程管理 # 添加到线程管理
ThreadManager.add(udid, thread, event) code , msg = ThreadManager.add(udid, thread, event)
return ResultData(data="").toJson() return ResultData(data="", code=code, message= msg).toJson()
# 观看直播 # 观看直播
@@ -268,7 +268,7 @@ def stopScript():
udid = body.get("udid") udid = body.get("udid")
LogManager.method_info(f"接口收到 /stopScript udid={udid}", method="task") LogManager.method_info(f"接口收到 /stopScript udid={udid}", method="task")
code, msg = ThreadManager.stop(udid) code, msg = ThreadManager.stop(udid)
return ResultData(code=code, data="", massage=msg).toJson() return ResultData(code=code, data="", message=msg).toJson()
# 关注打招呼 # 关注打招呼
@@ -302,6 +302,7 @@ def passAnchorData():
return ResultData(data="").toJson() return ResultData(data="").toJson()
except Exception as e: except Exception as e:
LogManager.error(e) LogManager.error(e)
return ResultData(data="", code=1001).toJson()
# 获取私信数据 # 获取私信数据
@@ -322,7 +323,7 @@ def addTempAnchorData():
""" """
data = request.get_json() data = request.get_json()
if not data: if not data:
return ResultData(code=400, massage="请求数据为空").toJson() return ResultData(code=400, message="请求数据为空").toJson()
# 追加到 JSON 文件 # 追加到 JSON 文件
AiUtils.save_aclist_flat_append(data, "log/acList.json") AiUtils.save_aclist_flat_append(data, "log/acList.json")
return ResultData(data="ok").toJson() return ResultData(data="ok").toJson()
@@ -356,7 +357,7 @@ def getChatTextInfo():
'text': 'Unable to retrieve chat messages on the current screen. Please navigate to the TikTok chat page and try again!!!' 'text': 'Unable to retrieve chat messages on the current screen. Please navigate to the TikTok chat page and try again!!!'
} }
] ]
return ResultData(data=data, massage="解析失败").toJson() return ResultData(data=data, message="解析失败").toJson()
# 监控消息 # 监控消息
@@ -383,7 +384,7 @@ def upLoadLogLogs():
if ok: if ok:
return ResultData(data="日志上传成功").toJson() return ResultData(data="日志上传成功").toJson()
else: else:
return ResultData(data="", massage="日志上传失败").toJson() return ResultData(data="", message="日志上传失败").toJson()
# 获取当前的主播列表数据 # 获取当前的主播列表数据
@@ -450,8 +451,8 @@ def update_last_message():
multi=False # 只改第一条匹配的 multi=False # 只改第一条匹配的
) )
if updated_count > 0: if updated_count > 0:
return ResultData(data=updated_count, massage="修改成功").toJson() return ResultData(data=updated_count, message="修改成功").toJson()
return ResultData(data=updated_count, massage="修改失败").toJson() return ResultData(data=updated_count, message="修改失败").toJson()
@app.route("/delete_last_message", methods=['POST']) @app.route("/delete_last_message", methods=['POST'])
@@ -467,8 +468,8 @@ def delete_last_message():
multi=False # 只改第一条匹配的 multi=False # 只改第一条匹配的
) )
if updated_count > 0: if updated_count > 0:
return ResultData(data=updated_count, massage="修改成功").toJson() return ResultData(data=updated_count, message="修改成功").toJson()
return ResultData(data=updated_count, massage="修改失败").toJson() return ResultData(data=updated_count, message="修改失败").toJson()
# @app.route("/killWda", methods=['POST']) # @app.route("/killWda", methods=['POST'])

View File

@@ -105,7 +105,6 @@ class FlaskSubprocessManager:
# 守护线程:把子进程 stdout → LogManager.info/system # 守护线程:把子进程 stdout → LogManager.info/system
threading.Thread(target=self._flush_stdout, daemon=True).start() threading.Thread(target=self._flush_stdout, daemon=True).start()
LogManager.info(f"Flask 子进程已启动PID={self.process.pid},端口={self.comm_port}", udid="system") LogManager.info(f"Flask 子进程已启动PID={self.process.pid},端口={self.comm_port}", udid="system")
if not self._wait_port_open(timeout=10): if not self._wait_port_open(timeout=10):
@@ -122,6 +121,8 @@ class FlaskSubprocessManager:
for line in iter(self.process.stdout.readline, ""): for line in iter(self.process.stdout.readline, ""):
if line: if line:
LogManager.info(line.rstrip(), udid="system") LogManager.info(line.rstrip(), udid="system")
# 同时输出到控制台
print(line.rstrip()) # 打印到主进程的控制台
self.process.stdout.close() self.process.stdout.close()
# ---------- 发送 ---------- # ---------- 发送 ----------

View File

@@ -1,12 +1,9 @@
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
from Module.DeviceInfo import Deviceinfo from Module.DeviceInfo import Deviceinfo
from Module.FlaskSubprocessManager import FlaskSubprocessManager from Module.FlaskSubprocessManager import FlaskSubprocessManager
from Utils.DevDiskImageDeployer import DevDiskImageDeployer from Utils.DevDiskImageDeployer import DevDiskImageDeployer
from Utils.LogManager import LogManager
# 确定 exe 或 py 文件所在目录 # 确定 exe 或 py 文件所在目录
BASE = Path(getattr(sys, 'frozen', False) and sys.executable or __file__).resolve().parent BASE = Path(getattr(sys, 'frozen', False) and sys.executable or __file__).resolve().parent

View File

@@ -28,43 +28,6 @@ def _force_utf8_everywhere():
# _force_utf8_everywhere() # _force_utf8_everywhere()
# ========= 全局:强制 UTF-8 + 关闭缓冲(运行期立刻生效) =========
def _force_utf8_everywhere():
os.environ.setdefault("PYTHONUTF8", "1")
# 等价于 -u让 stdout/stderr 无缓冲
os.environ.setdefault("PYTHONUNBUFFERED", "1")
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
# 若是 3.7+,优先用 reconfigure 实时改流
try:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace",
line_buffering=True, write_through=True)
elif getattr(sys.stdout, "buffer", None):
# 退路:重新包一层,启用行缓冲 + 直写
sys.stdout = io.TextIOWrapper(
sys.stdout.buffer, encoding="utf-8",
errors="replace", line_buffering=True
)
except Exception:
pass
try:
if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8", errors="replace",
line_buffering=True, write_through=True)
elif getattr(sys.stderr, "buffer", None):
sys.stderr = io.TextIOWrapper(
sys.stderr.buffer, encoding="utf-8",
errors="replace", line_buffering=True
)
except Exception:
pass
# ===========================================================
class LogManager: class LogManager:
""" """
设备级与“设备+方法”级日志管理: 设备级与“设备+方法”级日志管理:

View File

@@ -1,126 +1,77 @@
import os
import signal
import sys
import threading import threading
import time from typing import Dict, Tuple
import psutil
import subprocess
from pathlib import Path
from threading import Event, Thread
from typing import Dict, Optional
from Utils.LogManager import LogManager from Utils.LogManager import LogManager
class ThreadManager: class ThreadManager:
""" _tasks: Dict[str, Dict] = {}
对调用方完全透明:
add(udid, thread_obj, stop_event) 保持原签名
stop(udid) 保持原签名
但内部把 thread_obj 当成“壳”,真正拉起的是子进程。
"""
_pool: Dict[str, psutil.Process] = {}
_lock = threading.Lock() _lock = threading.Lock()
@classmethod @classmethod
def add(cls, udid: str, dummy_thread, dummy_event: Event) -> None: def add(cls, udid: str, thread: threading.Thread, event: threading.Event) -> Tuple[int, str]:
LogManager.method_info(f"【1】入口 udid={udid} 长度={len(udid)}", method="task") """
if udid in cls._pool: 添加一个线程到线程管理器。
LogManager.method_warning(f"{udid} 仍在运行,先强制清理旧任务", method="task") :param udid: 设备的唯一标识符
cls.stop(udid) :param thread: 线程对象
LogManager.method_info(f"【2】判断旧任务后 udid={udid} 长度={len(udid)}", method="task") :param event: 用于控制线程退出的 Event 对象
port = cls._find_free_port() :return: 状态码和信息
LogManager.method_info(f"【3】找端口后 udid={udid} 长度={len(udid)}", method="task") """
proc = cls._start_worker_process(udid, port) with cls._lock:
LogManager.method_info(f"【4】子进程启动后 udid={udid} 长度={len(udid)}", method="task") if udid in cls._tasks and cls._tasks[udid].get("running", False):
cls._pool[udid] = proc LogManager.method_info(f"任务添加失败:设备 {udid} 已存在运行中的任务", method="task")
LogManager.method_info(f"【5】已写入字典udid={udid} 长度={len(udid)}", method="task") return 400, f"该设备中已存在任务 {udid}"
# 如果任务已经存在但已停止,清理旧任务记录
if udid in cls._tasks and not cls._tasks[udid].get("running", False):
LogManager.method_info(f"清理设备 {udid} 的旧任务记录", method="task")
del cls._tasks[udid]
# 添加新任务记录
cls._tasks[udid] = {
"thread": thread,
"event": event,
"running": True
}
LogManager.method_info(f"设备 {udid} 开始任务成功", method="task")
return 200, f"创建任务成功 {udid}"
@classmethod @classmethod
def stop(cls, udid: str) -> tuple[int, str]: 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 :param udid: 设备的唯一标识符
源码环境python -m Module.Worker --udid=xxx :return: 状态码和信息
""" """
exe_path = Path(sys.executable).resolve() with cls._lock:
is_frozen = exe_path.suffix.lower() == ".exe" and exe_path.exists() if udid not in cls._tasks or not cls._tasks[udid].get("running", False):
LogManager.method_info(f"任务停止失败:设备 {udid} 没有执行相关任务", method="task")
return 400, f"当前设备没有执行相关任务 {udid}"
if is_frozen: task = cls._tasks[udid]
# 打包后 event = task["event"]
cmd = [str(exe_path), "--role=worker", f"--udid={udid}", f"--port={port}"] thread = task["thread"]
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 + 独立会话,父进程死也不影响 LogManager.method_info(f"设备 {udid} 的任务正在停止", method="task")
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): event.set()
for line in iter(proc.stdout.readline, ""):
if line: # 等待线程结束
LogManager.info(line.rstrip(), udid) thread.join(timeout=5) # 可设置超时时间,避免阻塞
proc.stdout.close()
# 清理任务记录
del cls._tasks[udid] # 删除任务记录
LogManager.method_info(f"设备 {udid} 的任务停止成功", method="task")
return 200, f"当前任务停止成功 {udid}"
@classmethod
def is_task_running(cls, udid: str) -> bool:
"""
检查任务是否正在运行。
:param udid: 设备的唯一标识符
:return: True 表示任务正在运行False 表示没有任务运行
"""
with cls._lock:
is_running = cls._tasks.get(udid, {}).get("running", False)
LogManager.method_info(f"检查设备 {udid} 的任务状态:{'运行中' if is_running else '未运行'}", method="task")
return is_running