临时提交

This commit is contained in:
2025-10-29 16:56:34 +08:00
parent 0c78a025f4
commit 9b923ebe49
21 changed files with 158 additions and 45 deletions

View File

@@ -7,12 +7,10 @@ import time
from pathlib import Path
from queue import Queue
from typing import Any, Dict
from Entity import Variables
from Utils.AiUtils import AiUtils
from Utils.IOSAIStorage import IOSAIStorage
from Utils.LogManager import LogManager
import tidevice
import wda
from flask import Flask, request
from flask_cors import CORS
@@ -107,7 +105,6 @@ def _apply_device_event(obj: Dict[str, Any]):
def _handle_conn(conn: socket.socket, addr):
"""统一的连接处理函数(外部全局,避免内嵌函数覆盖)"""
try:
# LogManager.info(f"[SOCKET][ACCEPT] from={addr}")
with conn:
buffer = ""
while True:

View File

@@ -72,7 +72,6 @@ class FlaskSubprocessManager:
env = os.environ.copy()
env["FLASK_COMM_PORT"] = str(self.comm_port)
exe_path = Path(sys.executable).resolve()
if exe_path.name.lower() in ("python.exe", "pythonw.exe"):
exe_path = Path(sys.argv[0]).resolve()

View File

@@ -4,6 +4,8 @@ import os
import sys
from pathlib import Path
from Utils.LogManager import LogManager
if "IOSAI_PYTHON" not in os.environ:
base_path = Path(sys.argv[0]).resolve()
base_dir = base_path.parent if base_path.is_file() else base_path
@@ -28,7 +30,7 @@ print(f"日志目录: {LOG_DIR}")
def _run_flask_role():
from Module import FlaskService
port = int(os.getenv("FLASK_COMM_PORT", "34567")) # 固定端口的兜底仍是 34567
port = int(os.getenv("FLASK_COMM_PORT", "34566")) # 固定端口的兜底仍是 34567
app_factory = getattr(FlaskService, "create_app", None)
app = app_factory() if callable(app_factory) else FlaskService.app
app.run(host="0.0.0.0", port=port + 1, debug=False, use_reloader=False)
@@ -37,19 +39,18 @@ if "--role=flask" in sys.argv:
_run_flask_role()
sys.exit(0)
# 启动锁
def main(arg):
print(arg)
if len(arg) != 2:
if len(arg) != 2 or arg[1] != "iosai":
sys.exit(0)
else:
if arg[1] != "iosai":
sys.exit(0)
# 项目入口
if __name__ == "__main__":
# 获取启动时候传递的参数
main(sys.argv)
# 清空日志
LogManager.clearLogs()
# main(sys.argv)
# 添加iOS开发包到电脑上
deployer = DevDiskImageDeployer(verbose=True)

View File

@@ -26,7 +26,7 @@ def _force_utf8_everywhere():
except Exception:
pass
_force_utf8_everywhere()
# _force_utf8_everywhere()
class LogManager:
"""

View File

@@ -1,16 +1,23 @@
import ctypes
import threading
import time
import os
import signal
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Dict, Tuple, List
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=KeyboardInterrupt) -> bool:
"""向指定线程抛异常"""
def _async_raise(tid: int, exc_type=SystemExit) -> bool:
"""向指定线程异步注入异常(仅对 Python 解释器栈可靠)"""
if not tid:
LogManager.method_error(f"强杀失败: 线程ID为空", "task")
LogManager.method_error("强杀失败: 线程ID为空", "task")
return False
try:
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
@@ -30,15 +37,70 @@ def _async_raise(tid: int, exc_type=KeyboardInterrupt) -> bool:
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 and not obj["thread"].is_alive():
cls._tasks.pop(udid, None)
LogManager.method_info(f"检测到 [{udid}] 线程已结束,自动清理。", "task")
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]:
@@ -51,19 +113,27 @@ class ThreadManager:
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(10):
# 等 ident 初始化
for _ in range(20):
if thread.ident:
break
time.sleep(0.05)
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, "创建成功"
@@ -81,6 +151,7 @@ class ThreadManager:
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")
@@ -88,56 +159,101 @@ class ThreadManager:
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():
# 先给 1 秒高频检查机会(很多 I/O 点会在这个窗口立刻感知到)
# 高频窗口 1s(很多 I/O 点会在这个窗口立刻感知到)
t0 = time.time()
while time.time() - t0 < 1.0 and thread.is_alive():
time.sleep(0.05)
# 再进入原有的 join 窗口
thread.join(timeout=stop_timeout)
if thread.is_alive():
LogManager.method_info(f"[{udid}] 协作超时 -> 尝试强杀", "task")
try:
_async_raise(tid) # 兜底:依然保留你的策略
except Exception as e:
LogManager.method_error(f"[{udid}] 强杀触发失败: {e}", "task")
thread.join(timeout=kill_timeout)
# 子进程先收拾(避免后台外部程序继续卡死)
cls._kill_child_pids(udid, child_pids)
if not thread.is_alive():
LogManager.method_info(f"[{udid}] 停止成功", "task")
else:
LogManager.method_error(f"[{udid}] 停止失败(线程卡死),已清理占位", "task")
# 正常 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:
cls._tasks.pop(udid, None)
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:
event = obj["event"]
event.set()
obj["thread"].join(timeout=2)
if obj["thread"].is_alive():
_async_raise(obj["id"])
obj["thread"].join(timeout=1)
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 = []