diff --git a/.idea/iOSAI.iml b/.idea/iOSAI.iml
index 894b6b0..6cb8b9a 100644
--- a/.idea/iOSAI.iml
+++ b/.idea/iOSAI.iml
@@ -4,7 +4,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index c27b771..db8786c 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -3,5 +3,5 @@
-
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 7bdb874..0f5a39b 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,7 +4,10 @@
-
+
+
+
+
@@ -34,9 +37,6 @@
-
-
-
{
"customColor": "",
"associatedIndex": 5
@@ -49,50 +49,44 @@
- {
+ "keyToString": {
+ "ASKED_ADD_EXTERNAL_FILES": "true",
+ "ASKED_MARK_IGNORED_FILES_AS_EXCLUDED": "true",
+ "Python.12.executor": "Run",
+ "Python.123.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/20250916ios/iOSAI/resources",
+ "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": "preferences.editor.code.editing",
+ "vue.rearranger.settings.migration": "true"
}
-}]]>
+}
-
-
-
-
+
@@ -101,12 +95,12 @@
-
+
-
+
@@ -129,7 +123,7 @@
-
+
@@ -161,7 +155,7 @@
-
+
@@ -170,35 +164,12 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -232,19 +203,17 @@
-
-
-
-
+
+
+
-
-
+
@@ -310,10 +279,6 @@
-
-
-
-
@@ -355,31 +320,7 @@
1757587781103
-
-
- 1758121742405
-
-
-
- 1758121742405
-
-
-
- 1758197707496
-
-
-
- 1758197707496
-
-
-
- 1758202317131
-
-
-
- 1758202317131
-
-
+
@@ -403,25 +344,22 @@
-
-
+
-
+
-
-
+
-
-
-
+
+
\ No newline at end of file
diff --git a/Entity/ResultData.py b/Entity/ResultData.py
index 31495c5..cf50762 100644
--- a/Entity/ResultData.py
+++ b/Entity/ResultData.py
@@ -2,15 +2,15 @@ import json
# 返回数据模型
class ResultData(object):
- def __init__(self, code=200, data=None, massage="获取成功"):
+ def __init__(self, code=200, data=None, message="获取成功"):
super(ResultData, self).__init__()
self.code = code
self.data = data
- self.massage = massage
+ self.message = message
def toJson(self):
return json.dumps({
"code": self.code,
"data": self.data,
- "massage": self.massage
+ "message": self.message
}, ensure_ascii=False) # ensure_ascii=False 确保中文不会被转义
\ No newline at end of file
diff --git a/Module/DeviceInfo.py b/Module/DeviceInfo.py
index a52d3a9..dde3e40 100644
--- a/Module/DeviceInfo.py
+++ b/Module/DeviceInfo.py
@@ -4,12 +4,12 @@ import signal
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
-
-import wda
-import threading
-import subprocess
from pathlib import Path
from typing import List, Dict, Optional
+import threading
+import subprocess
+
+import wda
from tidevice import Usbmux, ConnectionType
from tidevice._device import BaseDevice
from Entity.DeviceModel import DeviceModel
@@ -23,10 +23,8 @@ class Deviceinfo(object):
"""设备生命周期管理:以 deviceModelList 为唯一真理源"""
def __init__(self):
- ...
- # ✅ 新增:连接线程池(最大 6 并发)
+ # ✅ 连接线程池(最大 6 并发)
self._connect_pool = ThreadPoolExecutor(max_workers=6)
- ...
if os.name == "nt":
self._si = subprocess.STARTUPINFO()
@@ -44,11 +42,14 @@ class Deviceinfo(object):
self._lock = threading.Lock()
self._model_index: Dict[str, DeviceModel] = {} # udid -> model
- # ✅ 1. 失踪时间戳记录(替代原来的 miss_count)
+ # ✅ 失踪时间戳记录(替代原来的 miss_count)
self._last_seen: Dict[str, float] = {}
self._port_pool: List[int] = []
self._port_in_use: set[int] = set()
+ # ✅ 新增:全局 iproxy 进程注册表 udid -> Popen
+ self._iproxy_registry: Dict[str, subprocess.Popen] = {}
+
# region iproxy 初始化(原逻辑不变)
try:
self.iproxy_path = self._iproxy_path()
@@ -76,6 +77,9 @@ class Deviceinfo(object):
args = [str(self.iproxy_path), "-u", udid, str(local_port), str(remote_port)]
p = subprocess.Popen(args, **self._popen_kwargs)
+ # ✅ 注册到全局表
+ self._iproxy_registry[udid] = p
+
def _pipe_to_log(name: str, stream):
try:
for line in iter(stream.readline, ''):
@@ -127,6 +131,13 @@ class Deviceinfo(object):
for udid in need_remove:
self._remove_model(udid)
+ # ✅ 实时清理孤儿 iproxy(原 10 秒改为每次循环)
+ self._cleanup_orphan_iproxy()
+
+ # ✅ 设备全空时核平所有 iproxy
+ if not self.deviceModelList:
+ self._kill_all_iproxy()
+
# 2. 发现新设备 → 并发连接
with self._lock:
new_udids = [d.udid for d in lists
@@ -146,7 +157,7 @@ class Deviceinfo(object):
time.sleep(1)
# ------------------------------------------------------------------
- # ✅ 3. USB 层枚举 SN(跨平台)
+ # ✅ USB 层枚举 SN(跨平台)
# ------------------------------------------------------------------
def _usb_enumerate_sn(self) -> set[str]:
try:
@@ -155,7 +166,32 @@ class Deviceinfo(object):
except Exception:
return set()
- # ===================== 以下代码与原文件完全一致 =====================
+ # ----------------------------------------------------------
+ # ✅ 清理孤儿 iproxy
+ # ----------------------------------------------------------
+ def _cleanup_orphan_iproxy(self):
+ live_udids = set(self._model_index.keys())
+ for udid, proc in list(self._iproxy_registry.items()):
+ if udid not in live_udids:
+ LogManager.warning(f"发现孤儿 iproxy 进程,UDID 不在线:{udid},正在清理")
+ self._terminate_proc(proc)
+ self._iproxy_registry.pop(udid, None)
+
+ # ----------------------------------------------------------
+ # ✅ 核平所有 iproxy(Windows / macOS 通用)
+ # ----------------------------------------------------------
+ def _kill_all_iproxy(self):
+ try:
+ if os.name == "nt":
+ subprocess.run(["taskkill", "/F", "/IM", "iproxy.exe"], check=False)
+ else:
+ subprocess.run(["pkill", "-f", "iproxy"], check=False)
+ self._iproxy_registry.clear()
+ LogManager.info("已强制清理所有 iproxy 进程")
+ except Exception as e:
+ LogManager.warning(f"强制清理 iproxy 失败:{e}")
+
+ # -------------------- 以下代码与原文件完全一致 --------------------
def _wda_health_checker(self):
while True:
time.sleep(1)
@@ -229,6 +265,11 @@ class Deviceinfo(object):
print(f"【删】待杀进程数 count={len(to_kill)} udid={udid}")
LogManager.method_info(f"【删】待杀进程数 count={len(to_kill)} udid={udid}", method="device_count")
+ # ✅ 先清理注册表中的 iproxy
+ iproxy_proc = self._iproxy_registry.pop(udid, None)
+ if iproxy_proc:
+ self._terminate_proc(iproxy_proc)
+
for idx, item in enumerate(to_kill, 1):
print(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}")
LogManager.method_info(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}", method="device_count")
@@ -335,13 +376,14 @@ class Deviceinfo(object):
if not self._spawn_iproxy:
LogManager.error("iproxy 启动器未就绪", udid)
return None
- while self._port_in_use and self._is_port_open(port):
+ for attempt in range(5):
+ if not self._is_port_open(port):
+ break
+ LogManager.warning(f"端口 {port} 仍被占用,第 {attempt+1} 次重试释放", udid)
pid = self._get_pid_by_port(port)
if pid and pid != os.getpid():
- LogManager.warning(f"端口 {port} 仍被 PID {pid} 占用,尝试释放", udid)
self._kill_pid_gracefully(pid)
- else:
- break
+ time.sleep(0.2)
try:
p = self._spawn_iproxy(udid, port, 9100)
self._port_in_use.add(port)
@@ -405,4 +447,4 @@ class Deviceinfo(object):
for p in candidates:
if p.exists():
return p
- raise FileNotFoundError(f"iproxy not found, tried: {[str(c) for c in candidates]}")
+ raise FileNotFoundError(f"iproxy not found, tried: {[str(c) for c in candidates]}")
\ No newline at end of file
diff --git a/Module/FlaskService.py b/Module/FlaskService.py
index 6612b22..9d38520 100644
--- a/Module/FlaskService.py
+++ b/Module/FlaskService.py
@@ -244,8 +244,8 @@ def growAccount():
thread = threading.Thread(target=manager.growAccount, args=(udid, event))
thread.start()
# 添加到线程管理
- ThreadManager.add(udid, thread, event)
- return ResultData(data="").toJson()
+ code , msg = ThreadManager.add(udid, thread, event)
+ return ResultData(data="", code=code, message= msg).toJson()
# 观看直播
@@ -269,7 +269,7 @@ def stopScript():
udid = body.get("udid")
LogManager.method_info(f"接口收到 /stopScript udid={udid}", method="task")
code, msg = ThreadManager.stop(udid)
- return ResultData(code=code, data="", massage=msg).toJson()
+ return ResultData(code=code, data="", message=msg).toJson()
# 关注打招呼
@@ -305,6 +305,7 @@ def passAnchorData():
return ResultData(data="").toJson()
except Exception as e:
LogManager.error(e)
+ return ResultData(data="", code=1001).toJson()
# 获取私信数据
@@ -325,7 +326,7 @@ def addTempAnchorData():
"""
data = request.get_json()
if not data:
- return ResultData(code=400, massage="请求数据为空").toJson()
+ return ResultData(code=400, message="请求数据为空").toJson()
# 追加到 JSON 文件
AiUtils.save_aclist_flat_append(data, "log/acList.json")
return ResultData(data="ok").toJson()
@@ -359,7 +360,7 @@ def getChatTextInfo():
'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()
# 监控消息
@@ -386,7 +387,7 @@ def upLoadLogLogs():
if ok:
return ResultData(data="日志上传成功").toJson()
else:
- return ResultData(data="", massage="日志上传失败").toJson()
+ return ResultData(data="", message="日志上传失败").toJson()
# 获取当前的主播列表数据
@@ -517,8 +518,8 @@ def update_last_message():
multi=False # 只改第一条匹配的
)
if updated_count > 0:
- return ResultData(data=updated_count, massage="修改成功").toJson()
- return ResultData(data=updated_count, massage="修改失败").toJson()
+ return ResultData(data=updated_count, message="修改成功").toJson()
+ return ResultData(data=updated_count, message="修改失败").toJson()
@app.route("/delete_last_message", methods=['POST'])
@@ -534,8 +535,8 @@ def delete_last_message():
multi=False # 只改第一条匹配的
)
if updated_count > 0:
- return ResultData(data=updated_count, massage="修改成功").toJson()
- return ResultData(data=updated_count, massage="修改失败").toJson()
+ return ResultData(data=updated_count, message="修改成功").toJson()
+ return ResultData(data=updated_count, message="修改失败").toJson()
# @app.route("/killWda", methods=['POST'])
diff --git a/Module/FlaskSubprocessManager.py b/Module/FlaskSubprocessManager.py
index f948845..ec366d8 100644
--- a/Module/FlaskSubprocessManager.py
+++ b/Module/FlaskSubprocessManager.py
@@ -105,7 +105,6 @@ class FlaskSubprocessManager:
# 守护线程:把子进程 stdout → LogManager.info/system
threading.Thread(target=self._flush_stdout, daemon=True).start()
-
LogManager.info(f"Flask 子进程已启动,PID={self.process.pid},端口={self.comm_port}", udid="system")
if not self._wait_port_open(timeout=10):
@@ -122,6 +121,8 @@ class FlaskSubprocessManager:
for line in iter(self.process.stdout.readline, ""):
if line:
LogManager.info(line.rstrip(), udid="system")
+ # 同时输出到控制台
+ print(line.rstrip()) # 打印到主进程的控制台
self.process.stdout.close()
# ---------- 发送 ----------
diff --git a/Module/Main.py b/Module/Main.py
index c86317d..e8e5be3 100644
--- a/Module/Main.py
+++ b/Module/Main.py
@@ -1,12 +1,9 @@
import os
import sys
from pathlib import Path
-
from Module.DeviceInfo import Deviceinfo
from Module.FlaskSubprocessManager import FlaskSubprocessManager
from Utils.DevDiskImageDeployer import DevDiskImageDeployer
-from Utils.LogManager import LogManager
-
# 确定 exe 或 py 文件所在目录
BASE = Path(getattr(sys, 'frozen', False) and sys.executable or __file__).resolve().parent
diff --git a/SupportFiles/14.0.zip b/SupportFiles/14.0.zip
new file mode 100644
index 0000000..bd7a539
Binary files /dev/null and b/SupportFiles/14.0.zip differ
diff --git a/SupportFiles/14.1.zip b/SupportFiles/14.1.zip
new file mode 100644
index 0000000..ceb8328
Binary files /dev/null and b/SupportFiles/14.1.zip differ
diff --git a/SupportFiles/14.2.zip b/SupportFiles/14.2.zip
new file mode 100644
index 0000000..6190de1
Binary files /dev/null and b/SupportFiles/14.2.zip differ
diff --git a/SupportFiles/14.3.zip b/SupportFiles/14.3.zip
new file mode 100644
index 0000000..1203d70
Binary files /dev/null and b/SupportFiles/14.3.zip differ
diff --git a/SupportFiles/14.4.zip b/SupportFiles/14.4.zip
new file mode 100644
index 0000000..a1025db
Binary files /dev/null and b/SupportFiles/14.4.zip differ
diff --git a/SupportFiles/14.5.zip b/SupportFiles/14.5.zip
new file mode 100644
index 0000000..d91b781
Binary files /dev/null and b/SupportFiles/14.5.zip differ
diff --git a/SupportFiles/14.6.zip b/SupportFiles/14.6.zip
new file mode 100644
index 0000000..9ed5e41
Binary files /dev/null and b/SupportFiles/14.6.zip differ
diff --git a/SupportFiles/14.7.zip b/SupportFiles/14.7.zip
new file mode 100644
index 0000000..3a8f01a
Binary files /dev/null and b/SupportFiles/14.7.zip differ
diff --git a/SupportFiles/14.8.zip b/SupportFiles/14.8.zip
new file mode 100644
index 0000000..ea9c3a6
Binary files /dev/null and b/SupportFiles/14.8.zip differ
diff --git a/SupportFiles/15.0.zip b/SupportFiles/15.0.zip
new file mode 100644
index 0000000..a91df82
Binary files /dev/null and b/SupportFiles/15.0.zip differ
diff --git a/SupportFiles/15.1.zip b/SupportFiles/15.1.zip
new file mode 100644
index 0000000..bd07873
Binary files /dev/null and b/SupportFiles/15.1.zip differ
diff --git a/SupportFiles/15.2.zip b/SupportFiles/15.2.zip
new file mode 100644
index 0000000..134c25b
Binary files /dev/null and b/SupportFiles/15.2.zip differ
diff --git a/SupportFiles/15.3.zip b/SupportFiles/15.3.zip
new file mode 100644
index 0000000..1a75175
Binary files /dev/null and b/SupportFiles/15.3.zip differ
diff --git a/SupportFiles/15.4.zip b/SupportFiles/15.4.zip
new file mode 100644
index 0000000..b62fa2d
Binary files /dev/null and b/SupportFiles/15.4.zip differ
diff --git a/SupportFiles/15.5.zip b/SupportFiles/15.5.zip
new file mode 100644
index 0000000..d84ff97
Binary files /dev/null and b/SupportFiles/15.5.zip differ
diff --git a/SupportFiles/15.6.zip b/SupportFiles/15.6.zip
new file mode 100644
index 0000000..bc450f3
Binary files /dev/null and b/SupportFiles/15.6.zip differ
diff --git a/SupportFiles/15.7.zip b/SupportFiles/15.7.zip
new file mode 100644
index 0000000..13ab765
Binary files /dev/null and b/SupportFiles/15.7.zip differ
diff --git a/SupportFiles/15.8.zip b/SupportFiles/15.8.zip
new file mode 100644
index 0000000..f413db6
Binary files /dev/null and b/SupportFiles/15.8.zip differ
diff --git a/SupportFiles/16.0.zip b/SupportFiles/16.0.zip
new file mode 100644
index 0000000..ab9a02a
Binary files /dev/null and b/SupportFiles/16.0.zip differ
diff --git a/SupportFiles/16.1.zip b/SupportFiles/16.1.zip
new file mode 100644
index 0000000..07c9458
Binary files /dev/null and b/SupportFiles/16.1.zip differ
diff --git a/SupportFiles/16.2.zip b/SupportFiles/16.2.zip
new file mode 100644
index 0000000..b5f95da
Binary files /dev/null and b/SupportFiles/16.2.zip differ
diff --git a/SupportFiles/16.3.zip b/SupportFiles/16.3.zip
new file mode 100644
index 0000000..bc4e184
Binary files /dev/null and b/SupportFiles/16.3.zip differ
diff --git a/SupportFiles/16.4.zip b/SupportFiles/16.4.zip
new file mode 100644
index 0000000..8a785e3
Binary files /dev/null and b/SupportFiles/16.4.zip differ
diff --git a/SupportFiles/16.5.zip b/SupportFiles/16.5.zip
new file mode 100644
index 0000000..fc2ec0b
Binary files /dev/null and b/SupportFiles/16.5.zip differ
diff --git a/SupportFiles/16.6.zip b/SupportFiles/16.6.zip
new file mode 100644
index 0000000..1e4924d
Binary files /dev/null and b/SupportFiles/16.6.zip differ
diff --git a/SupportFiles/16.7.zip b/SupportFiles/16.7.zip
new file mode 100644
index 0000000..f2a36f8
Binary files /dev/null and b/SupportFiles/16.7.zip differ
diff --git a/Utils/ControlUtils.py b/Utils/ControlUtils.py
index 9a42eea..9f7048a 100644
--- a/Utils/ControlUtils.py
+++ b/Utils/ControlUtils.py
@@ -79,10 +79,9 @@ class ControlUtils(object):
"//Window[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]").exists:
back = session.xpath(
"//Window[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]")
-
if back.exists:
back.click()
- return True
+ return True
else:
return False
except Exception as e:
diff --git a/Utils/LogManager.py b/Utils/LogManager.py
index 6ace65c..0c8bb94 100644
--- a/Utils/LogManager.py
+++ b/Utils/LogManager.py
@@ -28,43 +28,6 @@ def _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:
"""
设备级与“设备+方法”级日志管理:
diff --git a/Utils/ThreadManager.py b/Utils/ThreadManager.py
index 3d1adce..6740d08 100644
--- a/Utils/ThreadManager.py
+++ b/Utils/ThreadManager.py
@@ -1,126 +1,77 @@
-import os
-import signal
-import sys
import threading
-import time
-import psutil
-import subprocess
-from pathlib import Path
-from threading import Event, Thread
-from typing import Dict, Optional
-
+from typing import Dict, Tuple
from Utils.LogManager import LogManager
class ThreadManager:
- """
- 对调用方完全透明:
- add(udid, thread_obj, stop_event) 保持原签名
- stop(udid) 保持原签名
- 但内部把 thread_obj 当成“壳”,真正拉起的是子进程。
- """
- _pool: Dict[str, psutil.Process] = {}
+ _tasks: Dict[str, Dict] = {}
_lock = threading.Lock()
@classmethod
- def add(cls, udid: str, dummy_thread, dummy_event: Event) -> None:
- LogManager.method_info(f"【1】入口 udid={udid} 长度={len(udid)}", method="task")
- if udid in cls._pool:
- LogManager.method_warning(f"{udid} 仍在运行,先强制清理旧任务", method="task")
- cls.stop(udid)
- LogManager.method_info(f"【2】判断旧任务后 udid={udid} 长度={len(udid)}", method="task")
- port = cls._find_free_port()
- LogManager.method_info(f"【3】找端口后 udid={udid} 长度={len(udid)}", method="task")
- proc = cls._start_worker_process(udid, port)
- LogManager.method_info(f"【4】子进程启动后 udid={udid} 长度={len(udid)}", method="task")
- cls._pool[udid] = proc
- LogManager.method_info(f"【5】已写入字典,udid={udid} 长度={len(udid)}", method="task")
+ def add(cls, udid: str, thread: threading.Thread, event: threading.Event) -> Tuple[int, str]:
+ """
+ 添加一个线程到线程管理器。
+ :param udid: 设备的唯一标识符
+ :param thread: 线程对象
+ :param event: 用于控制线程退出的 Event 对象
+ :return: 状态码和信息
+ """
+ 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 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
- 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:
+ def stop(cls, udid: str) -> Tuple[int, str]:
"""
- 真正拉起子进程:
- 打包环境:exe --udid=xxx
- 源码环境:python -m Module.Worker --udid=xxx
+ 停止指定设备的线程。
+ :param udid: 设备的唯一标识符
+ :return: 状态码和信息
"""
- exe_path = Path(sys.executable).resolve()
- is_frozen = exe_path.suffix.lower() == ".exe" and exe_path.exists()
+ 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}"
- if is_frozen:
- # 打包后
- cmd = [str(exe_path), "--role=worker", f"--udid={udid}", f"--port={port}"]
- 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)
+ task = cls._tasks[udid]
+ event = task["event"]
+ thread = task["thread"]
- # 核心:CREATE_NO_WINDOW + 独立会话,父进程死也不影响
- 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)
+ LogManager.method_info(f"设备 {udid} 的任务正在停止", method="task")
- @staticmethod
- def _log_stdout(proc: subprocess.Popen, udid: str):
- for line in iter(proc.stdout.readline, ""):
- if line:
- LogManager.info(line.rstrip(), udid)
- proc.stdout.close()
\ No newline at end of file
+ # 设置停止标志位
+ event.set()
+
+ # 等待线程结束
+ thread.join(timeout=5) # 可设置超时时间,避免阻塞
+
+ # 清理任务记录
+ 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
diff --git a/script/ScriptManager.py b/script/ScriptManager.py
index d457980..26b177d 100644
--- a/script/ScriptManager.py
+++ b/script/ScriptManager.py
@@ -60,8 +60,6 @@ class ScriptManager():
# 设置手机的节点深度为15,判断该页面是否正确
session.appium_settings({"snapshotMaxDepth": 15})
- # 判断当前页面上是否有推荐按钮
-
el = session.xpath(
'//XCUIElementTypeButton[@name="top_tabs_recomend" or @name="推荐" or @label="推荐"]'
)
@@ -274,7 +272,6 @@ class ScriptManager():
retries = 0
while not event.is_set():
try:
-
self.greetNewFollowers(udid, needReply, event)
except Exception as e:
@@ -294,13 +291,11 @@ class ScriptManager():
LogManager.method_info(f"是否要自动回复消息:{needReply}", "关注打招呼", udid)
# 先关闭Tik Tok
-
ControlUtils.closeTikTok(session, udid)
time.sleep(1)
# 重新打开Tik Tok
ControlUtils.openTikTok(session, udid)
-
time.sleep(3)
LogManager.method_info(f"重启tiktok", "关注打招呼", udid)
# 设置查找深度
@@ -374,6 +369,7 @@ class ScriptManager():
input.set_text(f"{aid or '暂无数据'}\n")
# 定位 "关注" 按钮 通过关注按钮的位置点击主播首页
+
session.appium_settings({"snapshotMaxDepth": 25})
try:
@@ -606,7 +602,6 @@ class ScriptManager():
time.sleep(3)
continue # 重新进入 while 循环,调用 monitorMessages
-
# 检查未读消息并回复
def monitorMessages(self, session, udid):