From 4dd3eb59a463d3ce5ff4d40ed977c5e4b2c9e0e5 Mon Sep 17 00:00:00 2001 From: milk <53408947@qq.com> Date: Mon, 22 Sep 2025 19:10:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=89=B9=E9=87=8F=E5=81=9C?= =?UTF-8?q?=E6=AD=A2=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/workspace.xml | 65 ++++--- Module/FlaskService.py | 9 +- .../__pycache__/FlaskService.cpython-312.pyc | Bin 27083 -> 26940 bytes Module/__pycache__/Main.cpython-312.pyc | Bin 2843 -> 2843 bytes Utils/ThreadManager.py | 163 ++++++------------ Utils/__pycache__/LogManager.cpython-312.pyc | Bin 14663 -> 14663 bytes .../__pycache__/ThreadManager.cpython-312.pyc | Bin 7680 -> 4451 bytes .../__pycache__/ScriptManager.cpython-312.pyc | Bin 41418 -> 41418 bytes 8 files changed, 85 insertions(+), 152 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 7b6aac9..95075d2 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -5,12 +5,11 @@ + + - - - - - + + - { + "keyToString": { + "ASKED_ADD_EXTERNAL_FILES": "true", + "ASKED_MARK_IGNORED_FILES_AS_EXCLUDED": "true", + "Python.12.executor": "Run", + "Python.123.executor": "Run", + "Python.DeviceInfo.executor": "Run", + "Python.Main.executor": "Run", + "Python.Test.executor": "Run", + "Python.test.executor": "Run", + "Python.tidevice_entry.executor": "Run", + "RunOnceActivity.ShowReadmeOnStart": "true", + "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", + "RunOnceActivity.git.unshallow": "true", + "SHARE_PROJECT_CONFIGURATION_FILES": "true", + "git-widget-placeholder": "main", + "javascript.nodejs.core.library.configured.version": "20.17.0", + "javascript.nodejs.core.library.typings.version": "20.17.58", + "last_opened_file_path": "C:/Users/zhangkai/Desktop/last-item/Storage", + "node.js.detected.package.eslint": "true", + "node.js.detected.package.tslint": "true", + "node.js.selected.package.eslint": "(autodetect)", + "node.js.selected.package.tslint": "(autodetect)", + "nodejs_package_manager_path": "npm", + "settings.editor.selected.configurable": "org.jetbrains.plugins.gitlab.GitLabSettingsConfigurable", + "two.files.diff.last.used.file": "E:/share/iOSAI/Module/FlaskService.py", + "vue.rearranger.settings.migration": "true" } -}]]> +} diff --git a/Module/FlaskService.py b/Module/FlaskService.py index 1473185..8c607d9 100644 --- a/Module/FlaskService.py +++ b/Module/FlaskService.py @@ -242,8 +242,7 @@ def growAccount(): manager = ScriptManager() event = threading.Event() # 启动脚本 - thread = threading.Thread(target=manager.growAccount, args=(udid, event)) - thread.start() + thread = threading.Thread(target=manager.growAccount, args=(udid, event,)) # 添加到线程管理 code, msg = ThreadManager.add(udid, thread, event) return ResultData(data="", code=code, message=msg).toJson() @@ -257,7 +256,6 @@ def watchLiveForGrowth(): manager = ScriptManager() event = threading.Event() thread = threading.Thread(target=manager.watchLiveForGrowth, args=(udid, event)) - thread.start() # 添加到线程管理 ThreadManager.add(udid, thread, event) return ResultData(data="").toJson() @@ -300,7 +298,6 @@ def passAnchorData(): event = threading.Event() # 启动脚本 thread = threading.Thread(target=manager.safe_greetNewFollowers, args=(udid, needReply, event)) - thread.start() # 添加到线程管理 ThreadManager.add(udid, thread, event) return ResultData(data="").toJson() @@ -545,8 +542,8 @@ def delete_last_message(): @app.route("/stopAllTask", methods=['POST']) def stopAllTask(): idList = request.get_json() - code, data, msg = ThreadManager.batch_stop(idList) - return ResultData(code, data, msg).toJson() + code, msg = ThreadManager.batch_stop(idList) + return ResultData(code, "", msg).toJson() # @app.route("/killWda", methods=['POST']) diff --git a/Module/__pycache__/FlaskService.cpython-312.pyc b/Module/__pycache__/FlaskService.cpython-312.pyc index 4f003367b53c77e9cbacbf332ec7f2acfbdafbad..18b093ab171cede223314af1956b074f0fb3d994 100644 GIT binary patch delta 784 zcmaKpUr1AN6vywm_d4Bnude)qH=VlXY&tjZHaDGrR>r0jp@9z(lBo=)f<3r_QCmzR zQVcUrm=A?QA%pE9xevVP ztiFZ(4W8FfB%WB(!ynGAcl+TAVl%J$$&GqTGfqhzB|^5uWSP#{*P|t50R5BzVX)J zgjhjtzg}9jZvv=s%sHPc(-if9Qlq`KQmSq21?a=5 zYl;gS4$eDz9PMsn*(H_=n+|FLO;VeH{nh9Ryyqxqja1l-cijTiAygIOs+$uIwFkH) zpCFTzAR9ipm!SlwnkJBKzLsA|NTGI;VwBA4@lNw2P+^_NBkXcQmeZ#>{X$@=Gwy%K zIb>d?cK*u8Tb`b>2A_jkbEy3;X05fsFEZSUA1JWVF>;ttG9sWVF^4Yn8T!BCJaTr{E_;(N>vOq+CwXIk$yL zA*HQ~yW{4VkOAi&od;&6sZmyxT#A#!MR-40Q&0g-N?nv`qGX@+I=BT|+dm!yHher{ F{tH;w+28;G delta 770 zcmXw0T}V@L6#t+9z1y63bDBC`W4g_+ExdC%ow_2Y&IPimeGrrwPF6D|2p#&e<;+M0 zGwGoDkmQ383wa6q78-L}U^9D!am9tfdr3c{`|ajd!rsVh}&^ zy>J6N%S?FDX4OPrsr@nU4x2gQ8}nUn$2nV%(Aj(%&}uKl-HIH1!Yi@MUX1Tr3h}1B zM*c_aF%%K&&G^-B!(mUY&~o@W0Ean_c||ru$Q`1@rmkY#_On95(E%`krrJq1qUbjv zm<`y^8Sq(cPVQxvge#}TvLaS-7>se7s$#Ni-x=+#7LSgLau%h6nMYB!Mvk7aLYI4 zn{NNC(xp^}gvziGT=K?SU#qIq8imsFGaG-^cjy~iDcPV(uZ-E?s=S2RaDm955}8+q z7aG1mgWz+8C^QKVkM^-}LNGKj3|tXRo&=R&m1bWP&y**qj+ZkqEjZf7GvJ=EezJ^( zl<@Y<6qSFFlE(LvVrS0->1?FA`G!281ZA^pxh)4 zIfl@i$7@ScT3bSEiw`HY&asY|XR>sbdBf$;1{XX_B}uM4#qkM_PjXe;Y?a{djj#?5 z-0*`TluuKMri%!bP3XpS+nh1(j9X&5BsjiPC0L}SnzY0)9nzvKRIVw8I!U#ssmRh^ S!4-M|oc(_{2JD!3$?_KrW7`1$ diff --git a/Module/__pycache__/Main.cpython-312.pyc b/Module/__pycache__/Main.cpython-312.pyc index 077ebd9c709050ba840e3b894f225747c88c6c65..284dcb7b3c63e9eba4f5518f0d68dddb55a1ca9f 100644 GIT binary patch delta 20 acmbO&Hd~DQG%qg~0}veKytt8DfExfcJOs}G delta 20 acmbO&Hd~DQG%qg~0}v>^y0DR3fExfdBn1!v diff --git a/Utils/ThreadManager.py b/Utils/ThreadManager.py index 857b996..845dd47 100644 --- a/Utils/ThreadManager.py +++ b/Utils/ThreadManager.py @@ -1,136 +1,73 @@ +import ctypes import threading -from concurrent.futures import ThreadPoolExecutor, as_completed from typing import Dict, Tuple, List + 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: _tasks: Dict[str, Dict] = {} _lock = threading.Lock() @classmethod def add(cls, udid: str, thread: threading.Thread, event: threading.Event) -> Tuple[int, str]: - """ - 添加一个线程到线程管理器。 - :param udid: 设备的唯一标识符 - :param thread: 线程对象 - :param event: 用于控制线程退出的 Event 对象 - :return: 状态码和信息 - """ + LogManager.method_info(f"准备创建任务:{udid}", "task") with cls._lock: - if udid in cls._tasks and cls._tasks[udid].get("running", False): - LogManager.method_info(f"任务添加失败:设备 {udid} 已存在运行中的任务", method="task") - return 400, f"该设备中已存在任务 {udid}" + # 判断当前设备是否有任务 + if cls._tasks.get(udid, None) is not None: + 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] = { + "id": thread.ident, "thread": thread, - "event": event, - "running": True + "event": event } - LogManager.method_info(f"设备 {udid} 开始任务成功", method="task") - return 200, f"创建任务成功 {udid}" + return 200, "创建成功" @classmethod def stop(cls, udid: str) -> Tuple[int, str]: - """ - 停止指定设备的线程。 - :param udid: 设备的唯一标识符 - :return: 状态码和信息 - """ - with cls._lock: - 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}" - - task = cls._tasks[udid] - event = task["event"] - 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}" + try: + print(cls._tasks) + obj = cls._tasks.get(udid, None) + obj["event"].set() + r = cls._kill_thread(obj.get("id")) + if r: + cls._tasks.pop(udid, None) + else: + print("好像有问题") + except Exception as e: + print(e) + return 200, "操作成功" @classmethod - def batch_stop(cls, udids: List[str]) -> Tuple[int, List[str], str]: - if not udids: - return 200, [], "无设备需要停止" - - fail_list: List[str] = [] - - # ---------- 1. 瞬间置位 event ---------- - 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, [], "全部设备停止成功" + def batch_stop(cls, ids: List[str]) -> Tuple[int, str]: + try: + for udid in ids: + cls.stop(udid) + except Exception as e: + print(e) + return 200, "停止成功." @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 + def _kill_thread(cls, tid: int) -> bool: + """向原生线程 ID 抛 KeyboardInterrupt,强制跳出""" + res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), + ctypes.py_object(KeyboardInterrupt)) + if res == 0: # 线程已不存在 + print("线程不存在") + return False + if res > 1: # 命中多个线程,重置 + print("命中了多个线程") + ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), 0) + print("杀死线程成功") + return True \ No newline at end of file diff --git a/Utils/__pycache__/LogManager.cpython-312.pyc b/Utils/__pycache__/LogManager.cpython-312.pyc index e8d07465556a14b4889341356139d7746934c48b..1a1e4120a9f536922dac4e8e46e0d1affff0bd29 100644 GIT binary patch delta 20 acmX?Jbi9cBG%qg~0}wp?d0`{BjU@n2wFgK5 delta 20 acmX?Jbi9cBG%qg~0}#k8J-3nD#u5NXPzEgk diff --git a/Utils/__pycache__/ThreadManager.cpython-312.pyc b/Utils/__pycache__/ThreadManager.cpython-312.pyc index 3d4d6c7e312151f47ce4763b2a02e125709030a5..c24066db8add9ef243a9c66b53e40dcc5e8024c5 100644 GIT binary patch literal 4451 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