From 2aba193e965f1a2ad5ac8d6dbe02ed239bdbda68 Mon Sep 17 00:00:00 2001 From: milk <53408947@qq.com> Date: Fri, 7 Nov 2025 21:37:25 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=81=9C=E6=AD=A2=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E9=80=BB=E8=BE=91=E4=B8=BA=E7=9B=B4=E6=8E=A5=E6=9D=80?= =?UTF-8?q?=E7=BA=BF=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__pycache__/FlaskService.cpython-312.pyc | Bin 43138 -> 43138 bytes Utils/ThreadManager.py | 796 ++++-------------- Utils/__pycache__/LogManager.cpython-312.pyc | Bin 14674 -> 14674 bytes .../__pycache__/ThreadManager.cpython-312.pyc | Bin 15625 -> 10572 bytes script/ScriptManager.py | 18 +- .../__pycache__/ScriptManager.cpython-312.pyc | Bin 70644 -> 70338 bytes 6 files changed, 193 insertions(+), 621 deletions(-) diff --git a/Module/__pycache__/FlaskService.cpython-312.pyc b/Module/__pycache__/FlaskService.cpython-312.pyc index 734e202e9ec6496271bcfa368c2e2080ddb40b36..ac77a514a7ada2e23201c749ac6707c6fbf0c1fe 100644 GIT binary patch delta 21 bcmZp=$kcR^iR&~kFBbz4*xcO6Rj~p9N#F*Q delta 21 bcmZp=$kcR^iR&~kFBbz4%-_0^t6~KJOMV8^ diff --git a/Utils/ThreadManager.py b/Utils/ThreadManager.py index ab4e561..ddd6804 100644 --- a/Utils/ThreadManager.py +++ b/Utils/ThreadManager.py @@ -1,635 +1,207 @@ -# import ctypes -# import threading -# import time -# import os -# import signal -# from concurrent.futures import ThreadPoolExecutor, as_completed -# from typing import Dict, Tuple, List, Optional -# -# from Utils.LogManager import LogManager -# -# try: -# import psutil # 可选:用来级联杀子进程 -# except Exception: -# psutil = None -# -# -# def _async_raise(tid: int, exc_type=SystemExit) -> bool: -# """向指定线程异步注入异常(仅对 Python 解释器栈可靠)""" -# if not tid: -# LogManager.method_error("强杀失败: 线程ID为空", "task") -# return False -# try: -# res = ctypes.pythonapi.PyThreadState_SetAsyncExc( -# ctypes.c_long(tid), ctypes.py_object(exc_type) -# ) -# if res == 0: -# LogManager.method_info(f"线程 {tid} 不存在", "task") -# return False -# elif res > 1: -# ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), 0) -# LogManager.method_info(f"线程 {tid} 命中多个线程,已回滚", "task") -# return False -# return True -# except Exception as e: -# LogManager.method_error(f"强杀线程失败: {e}", "task") -# return False -# -# -# class ThreadManager: -# """ -# 维持你的 add(udid, thread, event) 调用方式不变。 -# - 线程统一设为 daemon -# - 停止:协作 -> 多次强杀注入 -> zombie 放弃占位 -# - 可选:注册并级联杀掉业务里创建的外部子进程 -# """ -# _tasks: Dict[str, Dict] = {} -# _lock = threading.RLock() -# -# @classmethod -# def _cleanup_if_dead(cls, udid: str): -# obj = cls._tasks.get(udid) -# if obj: -# th = obj.get("thread") -# if th and not th.is_alive(): -# cls._tasks.pop(udid, None) -# LogManager.method_info(f"检测到 [{udid}] 线程已结束,自动清理。", "task") -# -# @classmethod -# def register_child_pid(cls, udid: str, pid: int): -# """业务里如果起了 adb/scrcpy/ffmpeg 等外部进程,请在启动后调用这个登记,便于 stop 时一起杀掉。""" -# with cls._lock: -# obj = cls._tasks.get(udid) -# if not obj: -# return -# pids: set = obj.setdefault("child_pids", set()) -# pids.add(int(pid)) -# LogManager.method_info(f"[{udid}] 记录子进程 PID={pid}", "task") -# -# @classmethod -# def _kill_child_pids(cls, udid: str, child_pids: Optional[set]): -# if not child_pids: -# return -# for pid in list(child_pids): -# try: -# if psutil: -# if psutil.pid_exists(pid): -# proc = psutil.Process(pid) -# # 先温柔 terminate,再等 0.5 秒,仍活则 kill;并级联子进程 -# for c in proc.children(recursive=True): -# try: -# c.terminate() -# except Exception: -# pass -# proc.terminate() -# gone, alive = psutil.wait_procs([proc], timeout=0.5) -# for a in alive: -# try: -# a.kill() -# except Exception: -# pass -# else: -# if os.name == "nt": -# os.system(f"taskkill /PID {pid} /T /F >NUL 2>&1") -# else: -# try: -# os.kill(pid, signal.SIGTERM) -# time.sleep(0.2) -# os.kill(pid, signal.SIGKILL) -# except Exception: -# pass -# LogManager.method_info(f"[{udid}] 已尝试结束子进程 PID={pid}", "task") -# except Exception as e: -# LogManager.method_error(f"[{udid}] 结束子进程 {pid} 异常: {e}", "task") -# -# @classmethod -# def add(cls, udid: str, thread: threading.Thread, event: threading.Event, force: bool = False) -> Tuple[int, str]: -# with cls._lock: -# cls._cleanup_if_dead(udid) -# old = cls._tasks.get(udid) -# if old and old["thread"].is_alive(): -# if not force: -# return 1001, "当前设备已存在任务" -# LogManager.method_info(f"[{udid}] 检测到旧任务,尝试强制停止", "task") -# cls._force_stop_locked(udid) -# -# # 强制守护线程,防止进程被挂死 -# try: -# thread.daemon = True -# except Exception: -# pass -# -# try: -# thread.start() -# # 等 ident 初始化 -# for _ in range(20): -# if thread.ident: -# break -# time.sleep(0.02) -# -# cls._tasks[udid] = { -# "id": thread.ident, -# "thread": thread, -# "event": event, -# "start_time": time.time(), -# "state": "running", -# "child_pids": set(), -# } -# LogManager.method_info(f"创建任务成功 [{udid}],线程ID={thread.ident}", "task") -# return 200, "创建成功" -# except Exception as e: -# LogManager.method_error(f"线程启动失败: {e}", "task") -# return 1002, f"线程启动失败: {e}" -# -# @classmethod -# def stop(cls, udid: str, stop_timeout: float = 5.0, kill_timeout: float = 2.0) -> Tuple[int, str]: -# with cls._lock: -# obj = cls._tasks.get(udid) -# if not obj: -# return 200, "任务不存在" -# -# thread = obj["thread"] -# event = obj["event"] -# tid = obj["id"] -# child_pids = set(obj.get("child_pids") or []) -# -# LogManager.method_info(f"请求停止 [{udid}] 线程ID={tid}", "task") -# -# if not thread.is_alive(): -# cls._tasks.pop(udid, None) -# return 200, "已结束" -# -# obj["state"] = "stopping" -# -# # 先把 event 打开,给协作退出的机会 -# try: -# event.set() -# except Exception as e: -# LogManager.method_error(f"[{udid}] 设置停止事件失败: {e}", "task") -# -# def _wait_stop(): -# # 高频窗口 1s(很多 I/O 点会在这个窗口立刻感知到) -# t0 = time.time() -# while time.time() - t0 < 1.0 and thread.is_alive(): -# time.sleep(0.05) -# -# # 子进程先收拾(避免后台外部程序继续卡死) -# cls._kill_child_pids(udid, child_pids) -# -# # 正常 join 窗口 -# if thread.is_alive(): -# thread.join(timeout=stop_timeout) -# -# # 仍活着 → 多次注入 SystemExit -# if thread.is_alive(): -# LogManager.method_info(f"[{udid}] 协作超时 -> 尝试强杀注入", "task") -# for i in range(6): -# ok = _async_raise(tid, SystemExit) -# # 给解释器一些调度时间 -# time.sleep(0.06) -# if not thread.is_alive(): -# break -# -# # 最后等待 kill_timeout -# if thread.is_alive(): -# thread.join(timeout=kill_timeout) -# -# with cls._lock: -# if not thread.is_alive(): -# LogManager.method_info(f"[{udid}] 停止成功", "task") -# cls._tasks.pop(udid, None) -# else: -# # 彻底杀不掉:标记 zombie、释放占位 -# LogManager.method_error(f"[{udid}] 停止失败(线程卡死),标记为 zombie,释放占位", "task") -# obj = cls._tasks.get(udid) -# if obj: -# obj["state"] = "zombie" -# cls._tasks.pop(udid, None) -# -# threading.Thread(target=_wait_stop, daemon=True).start() -# return 200, "停止请求已提交" -# -# @classmethod -# def _force_stop_locked(cls, udid: str): -# """持锁情况下的暴力停止(用于 add(force=True) 覆盖旧任务)""" -# obj = cls._tasks.get(udid) -# if not obj: -# return -# th = obj["thread"] -# tid = obj["id"] -# event = obj["event"] -# child_pids = set(obj.get("child_pids") or []) -# try: -# try: -# event.set() -# except Exception: -# pass -# cls._kill_child_pids(udid, child_pids) -# th.join(timeout=1.5) -# if th.is_alive(): -# for _ in range(6): -# _async_raise(tid, SystemExit) -# time.sleep(0.05) -# if not th.is_alive(): -# break -# th.join(timeout=0.8) -# except Exception as e: -# LogManager.method_error(f"[{udid}] 强制停止失败: {e}", "task") -# finally: -# cls._tasks.pop(udid, None) -# -# @classmethod -# def status(cls, udid: str) -> Dict: -# with cls._lock: -# obj = cls._tasks.get(udid) -# if not obj: -# return {"exists": False} -# o = { -# "exists": True, -# "state": obj.get("state"), -# "start_time": obj.get("start_time"), -# "thread_id": obj.get("id"), -# "alive": obj["thread"].is_alive(), -# "child_pids": list(obj.get("child_pids") or []), -# } -# return o -# -# # @classmethod -# # def batch_stop(cls, ids: List[str]) -> Tuple[int, str]: -# # failed = [] -# # with ThreadPoolExecutor(max_workers=4) as executor: -# # futures = {executor.submit(cls.stop, udid): udid for udid in ids} -# # for future in as_completed(futures): -# # udid = futures[future] -# # try: -# # code, msg = future.result() -# # except Exception as e: -# # LogManager.method_error(f"[{udid}] stop 调用异常: {e}", "task") -# # failed.append(udid) -# # continue -# # if code != 200: -# # failed.append(udid) -# # if failed: -# # return 207, f"部分任务停止失败: {failed}" -# # return 200, "全部停止请求已提交" -# -# @classmethod -# def batch_stop(cls, ids: List[str]) -> Tuple[int, str]: -# failed = [] -# results = [] -# -# with ThreadPoolExecutor(max_workers=4) as executor: -# futures = {executor.submit(cls.stop, udid): udid for udid in ids} -# for future in as_completed(futures): -# udid = futures[future] -# try: -# code, msg = future.result() -# results.append((udid, code, msg)) -# except Exception as e: -# LogManager.method_error(f"[{udid}] stop 调用异常: {e}", "task") -# failed.append(udid) -# continue -# if code != 200: -# failed.append(udid) -# -# # 等待所有线程完全停止 -# for udid, code, msg in results: -# if code == 200: -# obj = cls._tasks.get(udid) -# if obj: -# thread = obj["thread"] -# while thread.is_alive(): -# time.sleep(0.1) -# -# if failed: -# return 207, f"部分任务停止失败: {failed}" -# return 200, "全部任务已成功停止" +# -*- coding: utf-8 -*- - -import ctypes import threading +import ctypes +import inspect import time -import os -import signal -from concurrent.futures import ThreadPoolExecutor, as_completed -from typing import Dict, Tuple, List, Optional +from typing import Dict, Optional, List, Tuple, Any + +from Utils.LogManager import LogManager -# 假设 LogManager 存在 -class MockLogManager: - @staticmethod - def method_error(msg, category): - print(f"[ERROR:{category}] {msg}") - - @staticmethod - def method_info(msg, category): - print(f"[INFO:{category}] {msg}") +def _raise_async_exception(tid: int, exc_type) -> int: + if not inspect.isclass(exc_type): + raise TypeError("exc_type must be a class") + return ctypes.pythonapi.PyThreadState_SetAsyncExc( + ctypes.c_long(tid), ctypes.py_object(exc_type) + ) -LogManager = MockLogManager -# from Utils.LogManager import LogManager # 恢复实际导入 - -try: - import psutil # 可选:用来级联杀子进程 -except Exception: - psutil = None - - -def _async_raise(tid: int, exc_type=SystemExit) -> bool: - """ - 向指定线程异步注入异常。 - 注意:此方法在线程阻塞于C/OS调用(如I/O等待)时可能无效或延迟。 - """ - if not tid: - LogManager.method_error("强杀失败: 线程ID为空", "task") +def _kill_thread_by_tid(tid: Optional[int]) -> bool: + if tid is None: return False - try: - res = ctypes.pythonapi.PyThreadState_SetAsyncExc( - ctypes.c_long(tid), ctypes.py_object(exc_type) - ) - if res == 0: - # 线程可能已经退出 - return False - elif res > 1: - # 命中多个线程,非常罕见,回滚以防误杀 - ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), 0) - LogManager.method_info(f"线程 {tid} 命中多个线程,已回滚", "task") - return False - return True - except Exception as e: - LogManager.method_error(f"强杀线程失败: {e}", "task") + res = _raise_async_exception(tid, SystemExit) + if res == 0: return False + if res > 1: + _raise_async_exception(tid, None) + raise SystemError("PyThreadState_SetAsyncExc affected multiple threads; reverted.") + return True class ThreadManager: """ - 线程管理类:支持协作停止、强制注入SystemExit和级联杀死外部子进程。 - 注意:stop 方法已改为同步阻塞,直到线程真正停止或被标记为zombie。 + - add(udid, thread_or_target, *args, **kwargs) -> (code, msg) + - stop(udid, join_timeout=2.0, retries=5, wait_step=0.2) -> (code, msg) # 强杀 + - batch_stop(udids, join_timeout_each=2.0, retries_each=5, wait_step_each=0.2) -> (code, msg) + - get_thread / get_tid / is_running / list_udids """ - _tasks: Dict[str, Dict] = {} + _threads: Dict[str, threading.Thread] = {} _lock = threading.RLock() + # ========== 基础 ========== @classmethod - def _cleanup_if_dead(cls, udid: str): - obj = cls._tasks.get(udid) - if obj: - th = obj.get("thread") - if th and not th.is_alive(): - cls._tasks.pop(udid, None) - LogManager.method_info(f"检测到 [{udid}] 线程已结束,自动清理。", "task") - - @classmethod - def register_child_pid(cls, udid: str, pid: int): - """登记外部子进程 PID,便于 stop 时一起杀掉。""" - with cls._lock: - obj = cls._tasks.get(udid) - if not obj: - return - pids: set = obj.setdefault("child_pids", set()) - # 确保 pid 是 int 类型 - pids.add(int(pid)) - LogManager.method_info(f"[{udid}] 记录子进程 PID={pid}", "task") - - @classmethod - def _kill_child_pids(cls, udid: str, child_pids: Optional[set]): - """终止所有已登记的外部子进程及其子进程(重要:用于解决 I/O 阻塞)。""" - if not child_pids: - return - # 创建一个副本,防止迭代过程中集合被修改 - for pid in list(child_pids): - try: - if psutil: - if psutil.pid_exists(pid): - proc = psutil.Process(pid) - # 级联终止所有后代进程 - for c in proc.children(recursive=True): - try: - c.terminate() - except Exception: - pass - # 先温柔 terminate - proc.terminate() - gone, alive = psutil.wait_procs([proc], timeout=0.5) - # 仍活则 kill - for a in alive: - try: - a.kill() - except Exception: - pass - else: - # 无 psutil 时的系统命令兜底 - if os.name == "nt": - # /T 级联杀死子进程 /F 强制 /NUL 隐藏输出 - os.system(f"taskkill /PID {pid} /T /F >NUL 2>&1") - else: - try: - os.kill(pid, signal.SIGTERM) - time.sleep(0.2) - os.kill(pid, signal.SIGKILL) - except Exception: - pass - LogManager.method_info(f"[{udid}] 已尝试结束子进程 PID={pid}", "task") - except Exception as e: - LogManager.method_error(f"[{udid}] 结束子进程 {pid} 异常: {e}", "task") - - # 在同步停止模式下,这里不主动清理 set。 - - @classmethod - def add(cls, udid: str, thread: threading.Thread, event: threading.Event, force: bool = False) -> Tuple[int, str]: - with cls._lock: - cls._cleanup_if_dead(udid) - old = cls._tasks.get(udid) - if old and old["thread"].is_alive(): - if not force: - return 1001, "当前设备已存在任务" - LogManager.method_info(f"[{udid}] 检测到旧任务,尝试强制停止", "task") - cls._force_stop_locked(udid) - - # 强制守护线程,防止进程被挂死 - try: - thread.daemon = True - except Exception: - pass - - try: - thread.start() - # 等 ident 初始化 - for _ in range(20): - if thread.ident: - break - time.sleep(0.02) - - # 获取线程 ID - tid = thread.ident - - cls._tasks[udid] = { - "id": tid, - "thread": thread, - "event": event, - "start_time": time.time(), - "state": "running", - "child_pids": set(), - } - LogManager.method_info(f"创建任务成功 [{udid}],线程ID={tid}", "task") - return 200, "创建成功" - except Exception as e: - LogManager.method_error(f"线程启动失败: {e}", "task") - return 1002, f"线程启动失败: {e}" - - @classmethod - def stop(cls, udid: str, stop_timeout: float = 5.0, kill_timeout: float = 2.0) -> Tuple[int, str]: - """同步阻塞请求停止任务,直到线程真正停止或被标记为zombie。""" - # 1. 初始检查、状态设置和事件触发 (需要锁) - with cls._lock: - obj = cls._tasks.get(udid) - if not obj: - return 200, "任务不存在" - - thread = obj["thread"] - event = obj["event"] - tid = obj["id"] - # 拷贝 child_pids,以便在释放锁后使用 - child_pids = set(obj.get("child_pids") or []) - - LogManager.method_info(f"请求停止 [{udid}] 线程ID={tid}", "task") - - if not thread.is_alive(): - cls._tasks.pop(udid, None) - return 200, "已结束" - - obj["state"] = "stopping" - - # 先把 event 打开,给协作退出的机会 - try: - event.set() - except Exception as e: - LogManager.method_error(f"[{udid}] 设置停止事件失败: {e}", "task") - - # 锁已释放。以下执行耗时的阻塞操作。 - # ----------------- 阻塞停止逻辑开始 ----------------- - - # 2. 预等待窗口 1s - t0 = time.time() - while time.time() - t0 < 1.0 and thread.is_alive(): - time.sleep(0.05) - - # 3. 子进程先收拾 (优先解决 I/O 阻塞) - cls._kill_child_pids(udid, child_pids) - - # 4. 正常 join 窗口 - if thread.is_alive(): - thread.join(timeout=stop_timeout) - - # 5. 仍活着 → 多次注入 SystemExit - if thread.is_alive(): - LogManager.method_info(f"[{udid}] 协作超时 -> 尝试强杀注入", "task") - for i in range(6): - # 确保 tid 存在 - if tid: - _async_raise(tid, SystemExit) - time.sleep(0.06) - if not thread.is_alive(): - break - - # 6. 最后等待 kill_timeout - if thread.is_alive(): - thread.join(timeout=kill_timeout) - - # ----------------- 阻塞停止逻辑结束 ----------------- - # 7. 清理和返回结果 (需要重新加锁) - final_result_code: int = 500 - final_result_msg: str = "停止失败(线程卡死)" - - with cls._lock: - if not thread.is_alive(): - LogManager.method_info(f"[{udid}] 停止成功", "task") - cls._tasks.pop(udid, None) - final_result_code = 200 - final_result_msg = "停止成功" - else: - # 彻底杀不掉:标记 zombie、释放占位 - LogManager.method_error(f"[{udid}] 停止失败(线程卡死),标记为 zombie,释放占位", "task") - obj = cls._tasks.get(udid) - if obj: - obj["state"] = "zombie" - # 即使卡死,也移除任务记录,防止后续操作被阻塞 - cls._tasks.pop(udid, None) - final_result_code = 500 - final_result_msg = "停止失败(线程卡死)" - - return final_result_code, final_result_msg - - @classmethod - def _force_stop_locked(cls, udid: str): - """持锁情况下的暴力停止(用于 add(force=True) 覆盖旧任务)""" - obj = cls._tasks.get(udid) - if not obj: - return - th = obj["thread"] - tid = obj["id"] - event = obj["event"] - child_pids = set(obj.get("child_pids") or []) - try: - try: - event.set() - except Exception: - pass - cls._kill_child_pids(udid, child_pids) - th.join(timeout=1.5) - if th.is_alive(): - for _ in range(6): - if tid: - _async_raise(tid, SystemExit) - time.sleep(0.05) - if not th.is_alive(): - break - th.join(timeout=0.8) - except Exception as e: - LogManager.method_error(f"[{udid}] 强制停止失败: {e}", "task") - finally: - cls._tasks.pop(udid, None) - - @classmethod - def status(cls, udid: str) -> Dict: - with cls._lock: - obj = cls._tasks.get(udid) - if not obj: - return {"exists": False} - o = { - "exists": True, - "state": obj.get("state"), - "start_time": obj.get("start_time"), - "thread_id": obj.get("id"), - "alive": obj["thread"].is_alive(), - "child_pids": list(obj.get("child_pids") or []), - } - return o - - @classmethod - def batch_stop(cls, ids: List[str]) -> Tuple[int, str]: + def add(cls, udid: str, thread_or_target: Any, *args, **kwargs) -> Tuple[int, str]: """ - 批量停止任务。由于 stop 方法现在是同步阻塞的,此方法将等待所有线程完全停止后返回。 + 兼容两种用法: + 1) add(udid, t) # t 是 threading.Thread 实例 + 2) add(udid, target, *args, **kwargs) # target 是可调用 + 返回:(200, "创建任务成功") / (1001, "任务已存在") / (1001, "创建任务失败") """ - failed = [] + with cls._lock: + exist = cls._threads.get(udid) + if exist and exist.is_alive(): + return 1001, "任务已存在" - # 1. 并发发出所有停止请求 (现在是并发执行阻塞停止) - with ThreadPoolExecutor(max_workers=4) as executor: - futures = {executor.submit(cls.stop, udid): udid for udid in ids} - for future in as_completed(futures): - udid = futures[future] + if isinstance(thread_or_target, threading.Thread): + t = thread_or_target try: - code, msg = future.result() - # 检查是否成功停止(状态码 200) - if code != 200: - failed.append(f"{udid} ({msg})") - except Exception as e: - LogManager.method_error(f"[{udid}] stop 调用异常: {e}", "task") - failed.append(f"{udid} (异常)") + t.daemon = True + except Exception: + pass + if not t.name: + t.name = f"task-{udid}" - # 2. 返回结果 - if failed: - # 返回 207 表示部分失败或全部失败 - return 207, f"部分任务停止失败: {', '.join(failed)}" + # 包装 run,退出时从表移除 + orig_run = t.run + def run_wrapper(): + try: + orig_run() + finally: + with cls._lock: + if cls._threads.get(udid) is t: + cls._threads.pop(udid, None) + t.run = run_wrapper # type: ignore - # 返回 200 表示所有任务都已成功停止(因为 stop 方法是同步阻塞的) - return 200, "全部任务已成功停止" \ No newline at end of file + else: + target = thread_or_target + def _wrapper(): + try: + target(*args, **kwargs) + finally: + with cls._lock: + cur = cls._threads.get(udid) + if cur is threading.current_thread(): + cls._threads.pop(udid, None) + t = threading.Thread(target=_wrapper, daemon=True, name=f"task-{udid}") + + try: + t.start() + except Exception: + return 1001, "创建任务失败" + + cls._threads[udid] = t + # 保留你原有的创建成功日志 + try: + LogManager.method_info(f"创建任务成功 [{udid}],线程ID={t.ident}", "task") + except Exception: + pass + return 200, "创建任务成功" + + @classmethod + def get_thread(cls, udid: str) -> Optional[threading.Thread]: + with cls._lock: + return cls._threads.get(udid) + + @classmethod + def get_tid(cls, udid: str) -> Optional[int]: + t = cls.get_thread(udid) + return t.ident if t else None + + @classmethod + def is_running(cls, udid: str) -> bool: + t = cls.get_thread(udid) + return bool(t and t.is_alive()) + + @classmethod + def list_udids(cls) -> List[str]: + with cls._lock: + return list(cls._threads.keys()) + + # ========== 内部:强杀一次 ========== + + @classmethod + def _stop_once(cls, udid: str, join_timeout: float, retries: int, wait_step: float) -> bool: + """ + 对指定 udid 执行一次强杀流程;返回 True=已停止/不存在,False=仍存活或被拒。 + """ + with cls._lock: + t = cls._threads.get(udid) + + if not t: + return True # 视为已停止 + + main_tid = threading.main_thread().ident + cur_tid = threading.get_ident() + if t.ident in (main_tid, cur_tid): + return False + + try: + _kill_thread_by_tid(t.ident) + except Exception: + pass + + if join_timeout < 0: + join_timeout = 0.0 + t.join(join_timeout) + + while t.is_alive() and retries > 0: + evt = threading.Event() + evt.wait(wait_step) + retries -= 1 + + dead = not t.is_alive() + if dead: + with cls._lock: + if cls._threads.get(udid) is t: + cls._threads.pop(udid, None) + return dead + + # ========== 对外:stop / batch_stop(均返回二元组) ========== + + @classmethod + def stop(cls, udid: str, join_timeout: float = 2.0, + retries: int = 5, wait_step: float = 0.2) -> Tuple[int, str]: + """ + 强杀单个:返回 (200, "stopped") 或 (1001, "failed") + """ + ok = cls._stop_once(udid, join_timeout, retries, wait_step) + if ok: + return 200, "stopped" + else: + return 1001, "failed" + + @classmethod + def batch_stop(cls, udids: List[str], join_timeout_each: float = 2.0, + retries_each: int = 5, wait_step_each: float = 0.2) -> Tuple[int, str]: + """ + 先全量执行一遍 -> 记录失败 -> 对失败重试 3 轮(每轮间隔 1 秒) + 全部完成后统一返回: + 全成功 : (200, "停止任务成功") + 仍有失败: (1001, "停止任务失败") + """ + udids = udids or [] + fail: List[str] = [] + + # 第一轮 + for u in udids: + ok = cls._stop_once(u, join_timeout_each, retries_each, wait_step_each) + if not ok: + fail.append(u) + + # 三轮只对失败重试 + for _ in range(3): + if not fail: + break + time.sleep(1.0) + remain: List[str] = [] + for u in fail: + ok = cls._stop_once(u, join_timeout_each, retries_each, wait_step_each) + if not ok: + remain.append(u) + fail = remain + + if not fail: + return 200, "停止任务成功" + else: + return 1001, "停止任务失败" \ No newline at end of file diff --git a/Utils/__pycache__/LogManager.cpython-312.pyc b/Utils/__pycache__/LogManager.cpython-312.pyc index c4851fc6238ed6bc4029a956afb9f93c7f5ec089..89e42fe3d0a3e1c13274b6575aeafbd44ae6714e 100644 GIT binary patch delta 19 Zcmcaqbg78zG%qg~0}#Z%-pJ))2>?Wr1_l5C delta 19 Zcmcaqbg78zG%qg~0}$|ZZRGN>1OPzC1yle4 diff --git a/Utils/__pycache__/ThreadManager.cpython-312.pyc b/Utils/__pycache__/ThreadManager.cpython-312.pyc index 1bc0c8ce735d15bdf5d94482e17a41f4bde2b3ea..b13febb48bb4183ddd73fa816e05ef5d61480e43 100644 GIT binary patch literal 10572 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 XP8R{+u(DiBoztqzGnjMfG{pZ8Sc?^N diff --git a/script/ScriptManager.py b/script/ScriptManager.py index bf9f605..ef1241d 100644 --- a/script/ScriptManager.py +++ b/script/ScriptManager.py @@ -23,15 +23,15 @@ from Utils.TencentOCRUtils import TencentOCR # 脚本管理类 class ScriptManager(): - # 单利对象 - _instance = None # 类变量,用于存储单例实例 - - def __new__(cls): - # 如果实例不存在,则创建一个新实例 - if cls._instance is None: - cls._instance = super(ScriptManager, cls).__new__(cls) - # 返回已存在的实例 - return cls._instance + # # 单利对象 + # _instance = None # 类变量,用于存储单例实例 + # + # def __new__(cls): + # # 如果实例不存在,则创建一个新实例 + # if cls._instance is None: + # cls._instance = super(ScriptManager, cls).__new__(cls) + # # 返回已存在的实例 + # return cls._instance def __init__(self): super().__init__() diff --git a/script/__pycache__/ScriptManager.cpython-312.pyc b/script/__pycache__/ScriptManager.cpython-312.pyc index 964aef025cd519c8750c6483581f8f5949152f67..04de84f9ede3a1d597f48cf5e89c0882af7b96e0 100644 GIT binary patch delta 4447 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