优化批量停止脚本

This commit is contained in:
2025-09-22 19:10:58 +08:00
parent 81e3462f15
commit 4dd3eb59a4
8 changed files with 85 additions and 152 deletions

65
.idea/workspace.xml generated
View File

@@ -5,12 +5,11 @@
</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/2335890f77fb51322361bd46b85f7fd1311aed53/bgv.png" afterDir="false" />
<change afterPath="$PROJECT_DIR$/resources/3157d8bd35c230012cb3640d2f26ffd0b61a10d1/bgv.png" 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$/Module/DeviceInfo.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/DeviceInfo.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/Main.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/Main.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Utils/ThreadManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/ThreadManager.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Utils/AiUtils.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/AiUtils.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Utils/ControlUtils.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/ControlUtils.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Utils/JsonUtils.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/JsonUtils.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" />
@@ -53,35 +52,35 @@
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent"><![CDATA[{ <component name="PropertiesComponent">{
"keyToString": { &quot;keyToString&quot;: {
"ASKED_ADD_EXTERNAL_FILES": "true", &quot;ASKED_ADD_EXTERNAL_FILES&quot;: &quot;true&quot;,
"ASKED_MARK_IGNORED_FILES_AS_EXCLUDED": "true", &quot;ASKED_MARK_IGNORED_FILES_AS_EXCLUDED&quot;: &quot;true&quot;,
"Python.12.executor": "Run", &quot;Python.12.executor&quot;: &quot;Run&quot;,
"Python.123.executor": "Run", &quot;Python.123.executor&quot;: &quot;Run&quot;,
"Python.DeviceInfo.executor": "Run", &quot;Python.DeviceInfo.executor&quot;: &quot;Run&quot;,
"Python.Main.executor": "Run", &quot;Python.Main.executor&quot;: &quot;Run&quot;,
"Python.Test.executor": "Run", &quot;Python.Test.executor&quot;: &quot;Run&quot;,
"Python.test.executor": "Run", &quot;Python.test.executor&quot;: &quot;Run&quot;,
"Python.tidevice_entry.executor": "Run", &quot;Python.tidevice_entry.executor&quot;: &quot;Run&quot;,
"RunOnceActivity.ShowReadmeOnStart": "true", &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", &quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
"RunOnceActivity.git.unshallow": "true", &quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
"SHARE_PROJECT_CONFIGURATION_FILES": "true", &quot;SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
"git-widget-placeholder": "main", &quot;git-widget-placeholder&quot;: &quot;main&quot;,
"javascript.nodejs.core.library.configured.version": "20.17.0", &quot;javascript.nodejs.core.library.configured.version&quot;: &quot;20.17.0&quot;,
"javascript.nodejs.core.library.typings.version": "20.17.58", &quot;javascript.nodejs.core.library.typings.version&quot;: &quot;20.17.58&quot;,
"last_opened_file_path": "C:/Users/zhangkai/Desktop/last-item/Storage", &quot;last_opened_file_path&quot;: &quot;C:/Users/zhangkai/Desktop/last-item/Storage&quot;,
"node.js.detected.package.eslint": "true", &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
"node.js.detected.package.tslint": "true", &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
"node.js.selected.package.eslint": "(autodetect)", &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
"node.js.selected.package.tslint": "(autodetect)", &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
"nodejs_package_manager_path": "npm", &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
"settings.editor.selected.configurable": "org.jetbrains.plugins.gitlab.GitLabSettingsConfigurable", &quot;settings.editor.selected.configurable&quot;: &quot;org.jetbrains.plugins.gitlab.GitLabSettingsConfigurable&quot;,
"two.files.diff.last.used.file": "E:/share/iOSAI/Module/FlaskService.py", &quot;two.files.diff.last.used.file&quot;: &quot;E:/share/iOSAI/Module/FlaskService.py&quot;,
"vue.rearranger.settings.migration": "true" &quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
} }
}]]></component> }</component>
<component name="RecentsManager"> <component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS"> <key name="CopyFile.RECENT_KEYS">
<recent name="C:\Users\zhangkai\Desktop\last-item\iosai\Utils" /> <recent name="C:\Users\zhangkai\Desktop\last-item\iosai\Utils" />

View File

@@ -242,8 +242,7 @@ def growAccount():
manager = ScriptManager() manager = ScriptManager()
event = threading.Event() event = threading.Event()
# 启动脚本 # 启动脚本
thread = threading.Thread(target=manager.growAccount, args=(udid, event)) thread = threading.Thread(target=manager.growAccount, args=(udid, event,))
thread.start()
# 添加到线程管理 # 添加到线程管理
code, msg = ThreadManager.add(udid, thread, event) code, msg = ThreadManager.add(udid, thread, event)
return ResultData(data="", code=code, message=msg).toJson() return ResultData(data="", code=code, message=msg).toJson()
@@ -257,7 +256,6 @@ def watchLiveForGrowth():
manager = ScriptManager() manager = ScriptManager()
event = threading.Event() event = threading.Event()
thread = threading.Thread(target=manager.watchLiveForGrowth, args=(udid, event)) thread = threading.Thread(target=manager.watchLiveForGrowth, args=(udid, event))
thread.start()
# 添加到线程管理 # 添加到线程管理
ThreadManager.add(udid, thread, event) ThreadManager.add(udid, thread, event)
return ResultData(data="").toJson() return ResultData(data="").toJson()
@@ -300,7 +298,6 @@ def passAnchorData():
event = threading.Event() event = threading.Event()
# 启动脚本 # 启动脚本
thread = threading.Thread(target=manager.safe_greetNewFollowers, args=(udid, needReply, event)) thread = threading.Thread(target=manager.safe_greetNewFollowers, args=(udid, needReply, event))
thread.start()
# 添加到线程管理 # 添加到线程管理
ThreadManager.add(udid, thread, event) ThreadManager.add(udid, thread, event)
return ResultData(data="").toJson() return ResultData(data="").toJson()
@@ -545,8 +542,8 @@ def delete_last_message():
@app.route("/stopAllTask", methods=['POST']) @app.route("/stopAllTask", methods=['POST'])
def stopAllTask(): def stopAllTask():
idList = request.get_json() idList = request.get_json()
code, data, msg = ThreadManager.batch_stop(idList) code, msg = ThreadManager.batch_stop(idList)
return ResultData(code, data, msg).toJson() return ResultData(code, "", msg).toJson()
# @app.route("/killWda", methods=['POST']) # @app.route("/killWda", methods=['POST'])

View File

@@ -1,136 +1,73 @@
import ctypes
import threading import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Dict, Tuple, List from typing import Dict, Tuple, List
from Utils.LogManager import LogManager from Utils.LogManager import LogManager
def _async_raise(tid: int, exc_type=KeyboardInterrupt):
"""向指定线程抛异常,强制跳出"""
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), ctypes.py_object(exc_type))
if res == 0:
raise ValueError("线程不存在")
elif res > 1:
ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), 0)
class ThreadManager: class ThreadManager:
_tasks: Dict[str, Dict] = {} _tasks: Dict[str, Dict] = {}
_lock = threading.Lock() _lock = threading.Lock()
@classmethod @classmethod
def add(cls, udid: str, thread: threading.Thread, event: threading.Event) -> Tuple[int, str]: def add(cls, udid: str, thread: threading.Thread, event: threading.Event) -> Tuple[int, str]:
""" LogManager.method_info(f"准备创建任务:{udid}", "task")
添加一个线程到线程管理器。
:param udid: 设备的唯一标识符
:param thread: 线程对象
:param event: 用于控制线程退出的 Event 对象
:return: 状态码和信息
"""
with cls._lock: with cls._lock:
if udid in cls._tasks and cls._tasks[udid].get("running", False): # 判断当前设备是否有任务
LogManager.method_info(f"任务添加失败:设备 {udid} 已存在运行中的任务", method="task") if cls._tasks.get(udid, None) is not None:
return 400, f"设备已存在任务 {udid}" return 1001, "当前设备已存在任务"
thread.start()
print(thread.ident)
# 如果任务已经存在但已停止,清理旧任务记录
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] = { cls._tasks[udid] = {
"id": thread.ident,
"thread": thread, "thread": thread,
"event": event, "event": event
"running": True
} }
LogManager.method_info(f"设备 {udid} 开始任务成功", method="task") return 200, "创建成功"
return 200, f"创建任务成功 {udid}"
@classmethod @classmethod
def stop(cls, udid: str) -> Tuple[int, str]: def stop(cls, udid: str) -> Tuple[int, str]:
""" try:
停止指定设备的线程。 print(cls._tasks)
:param udid: 设备的唯一标识符 obj = cls._tasks.get(udid, None)
:return: 状态码和信息 obj["event"].set()
""" r = cls._kill_thread(obj.get("id"))
with cls._lock: if r:
if udid not in cls._tasks or not cls._tasks[udid].get("running", False): cls._tasks.pop(udid, None)
LogManager.method_info(f"任务停止失败:设备 {udid} 没有执行相关任务", method="task") else:
return 400, f"当前设备没有执行相关任务 {udid}" print("好像有问题")
except Exception as e:
task = cls._tasks[udid] print(e)
event = task["event"] return 200, "操作成功"
thread = task["thread"]
LogManager.method_info(f"设备 {udid} 的任务正在停止", method="task")
# 设置停止标志位
event.set()
# 等待线程结束
thread.join(timeout=5) # 可设置超时时间,避免阻塞
# 清理任务记录
del cls._tasks[udid] # 删除任务记录
LogManager.method_info(f"设备 {udid} 的任务停止成功", method="task")
return 200, f"当前任务停止成功 {udid}"
@classmethod @classmethod
def batch_stop(cls, udids: List[str]) -> Tuple[int, List[str], str]: def batch_stop(cls, ids: List[str]) -> Tuple[int, str]:
if not udids: try:
return 200, [], "无设备需要停止" for udid in ids:
cls.stop(udid)
fail_list: List[str] = [] except Exception as e:
print(e)
# ---------- 1. 瞬间置位 event ---------- return 200, "停止成功."
with cls._lock:
for udid in udids:
task = cls._tasks.get(udid)
if task and task.get("running"):
task["event"].set()
# ---------- 2. 单设备重试 5 次 ----------
def _wait_and_clean_with_retry(udid: str) -> Tuple[int, List[str], str]:
"""
内部重试 5 次,最终返回格式与 batch_stop 完全一致:
(code, fail_list, msg) fail_list 空表示成功
"""
for attempt in range(1, 6):
with cls._lock:
task = cls._tasks.get(udid)
if not task:
return 200, [], "任务已不存在" # 成功
thread = task["thread"]
thread.join(timeout=5)
still_alive = thread.is_alive()
# 最后一次重试才清理记录
if attempt == 5:
with cls._lock:
cls._tasks.pop(udid, None)
if not still_alive:
return 200, [], "已停止"
LogManager.warning(f"[batch_stop] {udid}{attempt}/5 次停止未立即结束,重试", udid)
# 5 次都失败
LogManager.error(f"[batch_stop] {udid} 停止失败5 次重试后线程仍活)", udid)
return 1001, [udid], "部分设备停止失败"
# ---------- 3. 并发执行 ----------
with ThreadPoolExecutor(max_workers=min(32, len(udids))) as executor:
future_map = {executor.submit(_wait_and_clean_with_retry, udid): udid for udid in udids}
for future in as_completed(future_map):
udid = future_map[future]
code, sub_fail_list, sub_msg = future.result() # ← 现在解包正确
if code != 200:
fail_list.extend(sub_fail_list) # 收集失败 udid
# ---------- 4. 返回兼容格式 ----------
if fail_list:
return 1001, fail_list, "部分设备停止失败"
return 200, [], "全部设备停止成功"
@classmethod @classmethod
def is_task_running(cls, udid: str) -> bool: def _kill_thread(cls, tid: int) -> bool:
""" """向原生线程 ID 抛 KeyboardInterrupt强制跳出"""
检查任务是否正在运行。 res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid),
:param udid: 设备的唯一标识符 ctypes.py_object(KeyboardInterrupt))
:return: True 表示任务正在运行False 表示没有任务运行 if res == 0: # 线程已不存在
""" print("线程不存在")
with cls._lock: return False
is_running = cls._tasks.get(udid, {}).get("running", False) if res > 1: # 命中多个线程,重置
LogManager.method_info(f"检查设备 {udid} 的任务状态:{'运行中' if is_running else '未运行'}", method="task") print("命中了多个线程")
return is_running ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), 0)
print("杀死线程成功")
return True