Merge remote-tracking branch 'origin/main'
# Conflicts: # .idea/workspace.xml # Module/DeviceInfo.py # Utils/ControlUtils.py # script/ScriptManager.py
This commit is contained in:
109
.idea/workspace.xml
generated
109
.idea/workspace.xml
generated
@@ -6,9 +6,15 @@
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成">
|
||||
<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$/Utils/ControlUtils.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/ControlUtils.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/script/ScriptManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/script/ScriptManager.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/build.bat" beforeDir="false" afterPath="$PROJECT_DIR$/build.bat" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/resources/add.png" beforeDir="false" afterPath="$PROJECT_DIR$/resources/add.png" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/resources/advertisement.png" beforeDir="false" afterPath="$PROJECT_DIR$/resources/advertisement.png" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/resources/back.png" beforeDir="false" afterPath="$PROJECT_DIR$/resources/back.png" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/resources/comment.png" beforeDir="false" afterPath="$PROJECT_DIR$/resources/comment.png" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/resources/fc18bc21951daf7be012a8a687b00a4de8b24c18/bgv.png" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/resources/icon.ico" beforeDir="false" afterPath="$PROJECT_DIR$/resources/icon.ico" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/resources/like.png" beforeDir="false" afterPath="$PROJECT_DIR$/resources/like.png" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/resources/search.png" beforeDir="false" afterPath="$PROJECT_DIR$/resources/search.png" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -51,33 +57,37 @@
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">{
|
||||
"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.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/20250916部署的ios项目/iOSAI",
|
||||
"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"
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"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"
|
||||
}
|
||||
}</component>
|
||||
}]]></component>
|
||||
<component name="RecentsManager">
|
||||
<key name="CopyFile.RECENT_KEYS">
|
||||
<recent name="C:\Users\zhangkai\Desktop\20250916ios\iOSAI\resources" />
|
||||
</key>
|
||||
<key name="MoveFile.RECENT_KEYS">
|
||||
<recent name="E:\Code\python\iOSAI\resources" />
|
||||
<recent name="E:\Code\python\iOSAI" />
|
||||
@@ -176,8 +186,32 @@
|
||||
<option name="INPUT_FILE" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="test" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||
<module name="iOSAI" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<envs>
|
||||
<env name="PYTHONUNBUFFERED" value="1" />
|
||||
</envs>
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Utils" />
|
||||
<option name="IS_MODULE_SDK" value="true" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/Utils/test.py" />
|
||||
<option name="PARAMETERS" value="" />
|
||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||
<option name="EMULATE_TERMINAL" value="false" />
|
||||
<option name="MODULE_MODE" value="false" />
|
||||
<option name="REDIRECT_INPUT" value="false" />
|
||||
<option name="INPUT_FILE" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<recent_temporary>
|
||||
<list>
|
||||
<item itemvalue="Python.test" />
|
||||
<item itemvalue="Python.123" />
|
||||
<item itemvalue="Python.Test" />
|
||||
<item itemvalue="Python.12" />
|
||||
@@ -187,8 +221,7 @@
|
||||
<component name="SharedIndexes">
|
||||
<attachedChunks>
|
||||
<set>
|
||||
<option value="bundled-js-predefined-1d06a55b98c1-0b3e54e931b4-JavaScript-PY-241.18034.82" />
|
||||
<option value="bundled-python-sdk-975db3bf15a3-2767605e8bc2-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-241.18034.82" />
|
||||
<option value="bundled-python-sdk-ce6832f46686-7b97d883f26b-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-252.25557.178" />
|
||||
</set>
|
||||
</attachedChunks>
|
||||
</component>
|
||||
@@ -254,18 +287,6 @@
|
||||
<workItem from="1757506636968" duration="5910000" />
|
||||
<workItem from="1757567423145" duration="16668000" />
|
||||
<workItem from="1757998910052" duration="3676000" />
|
||||
<workItem from="1758085730865" duration="22351000" />
|
||||
<workItem from="1758111847989" duration="1885000" />
|
||||
<workItem from="1758116448324" duration="460000" />
|
||||
<workItem from="1758116914311" duration="5000" />
|
||||
<workItem from="1758117364857" duration="749000" />
|
||||
<workItem from="1758118132336" duration="9000" />
|
||||
<workItem from="1758119195558" duration="42000" />
|
||||
<workItem from="1758119325860" duration="22000" />
|
||||
<workItem from="1758120216359" duration="1499000" />
|
||||
<workItem from="1758121819681" duration="179000" />
|
||||
<workItem from="1758122107459" duration="653000" />
|
||||
<workItem from="1758171695502" duration="333000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="ai 开始测试">
|
||||
<option name="closed" value="true" />
|
||||
@@ -346,7 +367,7 @@
|
||||
<SUITE FILE_PATH="coverage/iOSAI$123__1_.coverage" NAME="123 (1) 覆盖结果" MODIFIED="1756897091135" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||
<SUITE FILE_PATH="coverage/iOSAI$tidevice_entry.coverage" NAME="tidevice_entry 覆盖结果" MODIFIED="1757061969626" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||
<SUITE FILE_PATH="coverage/iOSAI$ScriptManager.coverage" NAME="ScriptManager 覆盖结果" MODIFIED="1756896057801" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" />
|
||||
<SUITE FILE_PATH="coverage/iOSAI$Main.coverage" NAME="Main 覆盖结果" MODIFIED="1758117454432" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="" />
|
||||
<SUITE FILE_PATH="coverage/iOSAI$123.coverage" NAME="123 覆盖结果" MODIFIED="1758002344317" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||
<SUITE FILE_PATH="coverage/iOSAI$Main.coverage" NAME="Main 覆盖结果" MODIFIED="1758120400301" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="" />
|
||||
<SUITE FILE_PATH="coverage/iOSAI$123.coverage" NAME="123 覆盖结果" MODIFIED="1758115088356" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,4 +1,4 @@
|
||||
from getpass import fallback_getpass
|
||||
# from getpass import fallback_getpass
|
||||
|
||||
|
||||
# 设备模型
|
||||
|
||||
BIN
Entity/__pycache__/AnchorModel.cpython-312.pyc
Normal file
BIN
Entity/__pycache__/AnchorModel.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Entity/__pycache__/DeviceModel.cpython-312.pyc
Normal file
BIN
Entity/__pycache__/DeviceModel.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Entity/__pycache__/ResultData.cpython-312.pyc
Normal file
BIN
Entity/__pycache__/ResultData.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Entity/__pycache__/Variables.cpython-312.pyc
Normal file
BIN
Entity/__pycache__/Variables.cpython-312.pyc
Normal file
Binary file not shown.
@@ -3,24 +3,38 @@ import os
|
||||
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
|
||||
|
||||
from tidevice import Usbmux, ConnectionType
|
||||
from tidevice._device import BaseDevice
|
||||
from Entity.DeviceModel import DeviceModel
|
||||
from Entity.Variables import WdaAppBundleId
|
||||
from Module.FlaskSubprocessManager import FlaskSubprocessManager
|
||||
from Utils.LogManager import LogManager
|
||||
from Utils.SubprocessKit import check_output as sp_check_output, popen as sp_popen
|
||||
|
||||
|
||||
class Deviceinfo(object):
|
||||
"""设备生命周期管理:以 deviceModelList 为唯一真理源"""
|
||||
|
||||
def __init__(self):
|
||||
...
|
||||
# ✅ 新增:连接线程池(最大 6 并发)
|
||||
self._connect_pool = ThreadPoolExecutor(max_workers=6)
|
||||
...
|
||||
|
||||
if os.name == "nt":
|
||||
self._si = subprocess.STARTUPINFO()
|
||||
self._si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
self._si.wShowWindow = subprocess.SW_HIDE # 0
|
||||
else:
|
||||
self._si = None
|
||||
|
||||
self.deviceIndex = 0
|
||||
self.screenProxy = 9110
|
||||
self.pidList: List[Dict] = [] # 仅记录 iproxy 进程
|
||||
@@ -30,14 +44,12 @@ class Deviceinfo(object):
|
||||
|
||||
self._lock = threading.Lock()
|
||||
self._model_index: Dict[str, DeviceModel] = {} # udid -> model
|
||||
self._miss_count: Dict[str, int] = {} # udid -> 连续未扫描到次数
|
||||
self._port_pool: List[int] = [] # 端口回收池
|
||||
self._port_in_use: set[int] = set() # 正在使用的端口
|
||||
# ✅ 1. 失踪时间戳记录(替代原来的 miss_count)
|
||||
self._last_seen: Dict[str, float] = {}
|
||||
self._port_pool: List[int] = []
|
||||
self._port_in_use: set[int] = set()
|
||||
|
||||
# 🔥1. 启动 WDA 健康检查线程
|
||||
# threading.Thread(target=self._wda_health_checker, daemon=True).start()
|
||||
|
||||
# region iproxy 初始化
|
||||
# region iproxy 初始化(原逻辑不变)
|
||||
try:
|
||||
self.iproxy_path = self._iproxy_path()
|
||||
self.iproxy_dir = self.iproxy_path.parent
|
||||
@@ -48,13 +60,14 @@ class Deviceinfo(object):
|
||||
pass
|
||||
|
||||
self._creationflags = 0x08000000 if os.name == "nt" else 0
|
||||
|
||||
self._popen_kwargs = dict(
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
cwd=str(self.iproxy_dir),
|
||||
shell=False,
|
||||
text=True,
|
||||
creationflags=self._creationflags,
|
||||
creationflags=0x08000000 if os.name == "nt" else 0, # CREATE_NO_WINDOW
|
||||
encoding="utf-8",
|
||||
bufsize=1,
|
||||
)
|
||||
@@ -85,7 +98,11 @@ class Deviceinfo(object):
|
||||
LogManager.error(f"初始化 iproxy 失败:{e}")
|
||||
# endregion
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 主监听循环 → 只负责“发现”和“提交任务”
|
||||
# ------------------------------------------------------------------
|
||||
def startDeviceListener(self):
|
||||
MISS_WINDOW = 5.0
|
||||
while True:
|
||||
try:
|
||||
lists = Usbmux().device_list()
|
||||
@@ -95,50 +112,55 @@ class Deviceinfo(object):
|
||||
continue
|
||||
|
||||
now_udids = {d.udid for d in lists if d.conn_type == ConnectionType.USB}
|
||||
usb_sn_set = self._usb_enumerate_sn()
|
||||
|
||||
# 1. 失踪登记 & 累加
|
||||
need_remove = None # ← 新增:放锁外记录
|
||||
# 1. 失踪判定(同旧逻辑)
|
||||
need_remove = []
|
||||
with self._lock:
|
||||
for udid in list(self._model_index.keys()):
|
||||
if udid not in now_udids:
|
||||
self._miss_count[udid] = self._miss_count.get(udid, 0) + 1
|
||||
if self._miss_count[udid] >= 3:
|
||||
self._miss_count.pop(udid, None)
|
||||
need_remove = udid # ← 只记录,不调用
|
||||
last = self._last_seen.get(udid, time.time())
|
||||
if time.time() - last > MISS_WINDOW and udid not in usb_sn_set:
|
||||
need_remove.append(udid)
|
||||
else:
|
||||
self._miss_count.pop(udid, None)
|
||||
self._last_seen[udid] = time.time()
|
||||
for udid in need_remove:
|
||||
self._remove_model(udid)
|
||||
|
||||
# 🔓 锁已释放,再删设备(不会重入)
|
||||
if need_remove:
|
||||
self._remove_model(need_remove)
|
||||
|
||||
# 2. 全新插入(只处理未在线且信任且未满)
|
||||
for d in lists:
|
||||
if d.conn_type != ConnectionType.USB:
|
||||
continue
|
||||
udid = d.udid
|
||||
with self._lock:
|
||||
if udid in self._model_index:
|
||||
continue
|
||||
if not self.is_device_trusted(udid):
|
||||
continue
|
||||
if len(self.deviceModelList) >= self.maxDeviceCount:
|
||||
continue
|
||||
try:
|
||||
self.connectDevice(udid)
|
||||
except Exception as e:
|
||||
LogManager.error(f"连接设备失败 {udid}: {e}", udid)
|
||||
# 2. 发现新设备 → 并发连接
|
||||
with self._lock:
|
||||
new_udids = [d.udid for d in lists
|
||||
if d.conn_type == ConnectionType.USB and
|
||||
d.udid not in self._model_index and
|
||||
len(self.deviceModelList) < self.maxDeviceCount]
|
||||
if new_udids:
|
||||
futures = {self._connect_pool.submit(self._connect_device_task, udid): udid
|
||||
for udid in new_udids}
|
||||
for f in as_completed(futures, timeout=10):
|
||||
udid = futures[f]
|
||||
try:
|
||||
f.result(timeout=8) # 单台 8 s 硬截止
|
||||
except Exception as e:
|
||||
LogManager.error(f"连接任务超时/失败: {e}", udid)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# 🔥2. WDA 健康检查
|
||||
# ------------------------------------------------------------------
|
||||
# ✅ 3. USB 层枚举 SN(跨平台)
|
||||
# ------------------------------------------------------------------
|
||||
def _usb_enumerate_sn(self) -> set[str]:
|
||||
try:
|
||||
out = sp_check_output(["idevice_id", "-l"], text=True, timeout=3)
|
||||
return {line.strip() for line in out.splitlines() if line.strip()}
|
||||
except Exception:
|
||||
return set()
|
||||
|
||||
# ===================== 以下代码与原文件完全一致 =====================
|
||||
def _wda_health_checker(self):
|
||||
while True:
|
||||
time.sleep(1)
|
||||
print(len(self.deviceModelList))
|
||||
with self._lock:
|
||||
online = [m for m in self.deviceModelList if m.ready] # ← 只检查就绪的
|
||||
print(len(online))
|
||||
online = [m for m in self.deviceModelList if m.ready]
|
||||
for model in online:
|
||||
udid = model.deviceId
|
||||
if not self._wda_ok(udid):
|
||||
@@ -147,32 +169,24 @@ class Deviceinfo(object):
|
||||
self._remove_model(udid)
|
||||
self.connectDevice(udid)
|
||||
|
||||
# 🔥3. 真正做 health-check 的地方
|
||||
def _wda_ok(self, udid: str) -> bool:
|
||||
"""返回 True 表示 WDA 活着,False 表示已死"""
|
||||
try:
|
||||
# 用 2 秒超时快速探测
|
||||
c = wda.USBClient(udid, 8100)
|
||||
# 下面这句就是“xctest launched but check failed” 的触发点
|
||||
# 如果 status 里返回了 WebDriverAgent 运行信息就认为 OK
|
||||
st = c.status()
|
||||
if st.get("state") != "success":
|
||||
return False
|
||||
# 你也可以再苛刻一点,多做一次 /wda/healthcheck
|
||||
# c.http.get("/wda/healthcheck")
|
||||
return True
|
||||
except Exception as e:
|
||||
# 任何异常(连接拒绝、超时、json 解析失败)都认为已死
|
||||
LogManager.error(f"WDA health-check 异常:{e}", udid)
|
||||
return False
|
||||
|
||||
# region ===================== 增删改查唯一入口(线程安全) =====================
|
||||
# -------------------- 增删改查唯一入口(未改动) --------------------
|
||||
def _has_model(self, udid: str) -> bool:
|
||||
return udid in self._model_index
|
||||
|
||||
def _add_model(self, model: DeviceModel):
|
||||
if model.deviceId in self._model_index:
|
||||
return # 防重复
|
||||
return
|
||||
model.ready = True
|
||||
self.deviceModelList.append(model)
|
||||
self._model_index[model.deviceId] = model
|
||||
@@ -180,60 +194,48 @@ class Deviceinfo(object):
|
||||
self.manager.send(model.toDict())
|
||||
except Exception as e:
|
||||
LogManager.warning(f"{model.deviceId} 发送上线事件失败:{e}")
|
||||
LogManager.method_info(f"{model.deviceId} 加入设备成功,当前在线数:{len(self.deviceModelList)}",method="device_count")
|
||||
LogManager.method_info(f"{model.deviceId} 加入设备成功,当前在线数:{len(self.deviceModelList)}", method="device_count")
|
||||
|
||||
|
||||
# 删除设备
|
||||
def _remove_model(self, udid: str):
|
||||
print(f"【删】进入删除方法 udid={udid}")
|
||||
LogManager.method_info(f"【删】进入删除方法 udid={udid}", method="device_count")
|
||||
# 1. 纯内存临界区——毫秒级
|
||||
with self._lock:
|
||||
print(f"【删】拿到锁 udid={udid}")
|
||||
LogManager.method_info(f"【删】拿到锁 udid={udid}",
|
||||
method="device_count")
|
||||
LogManager.method_info(f"【删】拿到锁 udid={udid}", method="device_count")
|
||||
model = self._model_index.pop(udid, None)
|
||||
if not model:
|
||||
print(f"【删】模型已空,直接返回 udid={udid}")
|
||||
LogManager.method_info(f"【删】模型已空,直接返回 udid={udid}",method="device_count")
|
||||
LogManager.method_info(f"【删】模型已空,直接返回 udid={udid}", method="device_count")
|
||||
return
|
||||
if model.deleting:
|
||||
print(f"【删】正在删除中,幂等返回 udid={udid}")
|
||||
LogManager.method_info(method="device_count", text=f"【删】正在删除中,幂等返回 udid={udid}")
|
||||
return
|
||||
model.deleting = True
|
||||
# 标记维删除设备
|
||||
model.type = 2
|
||||
print(f"【删】标记 deleting=True udid={udid}")
|
||||
LogManager.method_info("【删】标记 deleting=True udid={udid}","device_count")
|
||||
# 过滤列表
|
||||
LogManager.method_info("【删】标记 deleting=True udid={udid}", "device_count")
|
||||
before = len(self.deviceModelList)
|
||||
self.deviceModelList = [m for m in self.deviceModelList if m.deviceId != udid]
|
||||
after = len(self.deviceModelList)
|
||||
print(f"【删】列表过滤 before={before} → after={after} udid={udid}")
|
||||
LogManager.method_info(f"【删】列表过滤 before={before} → after={after} udid={udid}","device_count")
|
||||
|
||||
# 端口
|
||||
LogManager.method_info(f"【删】列表过滤 before={before} → after={after} udid={udid}", "device_count")
|
||||
self._port_in_use.discard(model.screenPort)
|
||||
self._port_pool.append(model.screenPort)
|
||||
print(f"【删】回收端口 port={model.screenPort} udid={udid}")
|
||||
LogManager.method_info(f"【删】回收端口 port={model.screenPort} udid={udid}", method="device_count")
|
||||
|
||||
# 进程
|
||||
to_kill = [item for item in self.pidList if item.get("id") == udid]
|
||||
self.pidList = [item for item in self.pidList if item.get("id") != udid]
|
||||
print(f"【删】待杀进程数 count={len(to_kill)} udid={udid}")
|
||||
LogManager.method_info(f"【删】待杀进程数 count={len(to_kill)} udid={udid}", method="device_count")
|
||||
|
||||
# 2. IO 区无锁
|
||||
for idx, item in enumerate(to_kill, 1):
|
||||
print(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}")
|
||||
LogManager.method_error(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}", method="device_count")
|
||||
LogManager.method_info(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}", method="device_count")
|
||||
self._terminate_proc(item.get("target"))
|
||||
print(f"【删】进程清理完成 udid={udid}")
|
||||
LogManager.method_info(f"【删】进程清理完成 udid={udid}", method="device_count")
|
||||
|
||||
# 3. 网络 IO
|
||||
retry = 3
|
||||
while retry:
|
||||
try:
|
||||
@@ -255,7 +257,7 @@ class Deviceinfo(object):
|
||||
print(len(self.deviceModelList))
|
||||
LogManager.method_info(f"当前剩余设备数量:{len(self.deviceModelList)}", method="device_count")
|
||||
|
||||
# region ===================== 端口分配与回收 =====================
|
||||
# -------------------- 端口分配与回收(未改动) --------------------
|
||||
def _alloc_port(self) -> int:
|
||||
if self._port_pool:
|
||||
port = self._port_pool.pop()
|
||||
@@ -269,14 +271,14 @@ class Deviceinfo(object):
|
||||
if port in self._port_in_use:
|
||||
self._port_in_use.remove(port)
|
||||
self._port_pool.append(port)
|
||||
# endregion
|
||||
|
||||
# region ===================== 单台设备连接 =====================
|
||||
def connectDevice(self, udid: str):
|
||||
# ------------------------------------------------------------------
|
||||
# 线程池里真正干活的地方(原 connectDevice 逻辑搬过来)
|
||||
# ------------------------------------------------------------------
|
||||
def _connect_device_task(self, udid: str):
|
||||
if not self.is_device_trusted(udid):
|
||||
LogManager.warning("设备未信任,跳过 WDA 启动", udid)
|
||||
return
|
||||
|
||||
try:
|
||||
d = wda.USBClient(udid, 8100)
|
||||
except Exception as e:
|
||||
@@ -293,23 +295,34 @@ class Deviceinfo(object):
|
||||
|
||||
port = self._alloc_port()
|
||||
model = DeviceModel(udid, port, width, height, scale, type=1)
|
||||
self._add_model(model)
|
||||
|
||||
# 先做完所有 IO,再抢锁写内存
|
||||
try:
|
||||
d.app_start(WdaAppBundleId)
|
||||
d.home()
|
||||
except Exception as e:
|
||||
LogManager.warning(f"启动/切回桌面失败:{e}", udid)
|
||||
|
||||
time.sleep(2)
|
||||
time.sleep(2) # 原逻辑保留
|
||||
|
||||
# 先清旧进程再启动新进程
|
||||
self.pidList = [item for item in self.pidList if item.get("id") != udid]
|
||||
target = self.relayDeviceScreenPort(udid, port)
|
||||
if target:
|
||||
self.pidList.append({"target": target, "id": udid})
|
||||
|
||||
# region ===================== 工具方法 =====================
|
||||
# 毫秒级临界区
|
||||
with self._lock:
|
||||
if udid in self._model_index: # 并发防重
|
||||
return
|
||||
self._add_model(model)
|
||||
if target:
|
||||
self.pidList.append({"target": target, "id": udid})
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 原函数保留(改名即可)
|
||||
# ------------------------------------------------------------------
|
||||
def connectDevice(self, udid: str):
|
||||
"""对外保留接口,实际走线程池"""
|
||||
self._connect_pool.submit(self._connect_device_task, udid)
|
||||
|
||||
# -------------------- 工具方法(未改动) --------------------
|
||||
def is_device_trusted(self, udid: str) -> bool:
|
||||
try:
|
||||
d = BaseDevice(udid)
|
||||
@@ -319,22 +332,16 @@ class Deviceinfo(object):
|
||||
return False
|
||||
|
||||
def relayDeviceScreenPort(self, udid: str, port: int) -> Optional[subprocess.Popen]:
|
||||
"""启动 iproxy 前:端口若仍被占用则先杀掉占用者,再启动"""
|
||||
if not self._spawn_iproxy:
|
||||
LogManager.error("iproxy 启动器未就绪", udid)
|
||||
return None
|
||||
|
||||
# --- 新增:端口冲突检查 + 强制清理 ---
|
||||
while self._port_in_use and self._is_port_open(port):
|
||||
# 先查是哪个进程占用
|
||||
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
|
||||
# -------------------------------------
|
||||
|
||||
try:
|
||||
p = self._spawn_iproxy(udid, port, 9100)
|
||||
self._port_in_use.add(port)
|
||||
@@ -344,30 +351,25 @@ class Deviceinfo(object):
|
||||
LogManager.error(f"启动 iproxy 失败:{e}", udid)
|
||||
return None
|
||||
|
||||
# ------------------- 新增三个小工具 -------------------
|
||||
def _is_port_open(self, port: int) -> bool:
|
||||
import socket
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
return s.connect_ex(("127.0.0.1", port)) == 0
|
||||
|
||||
def _get_pid_by_port(self, port: int) -> Optional[int]:
|
||||
"""跨平台根据端口号查 PID,失败返回 None"""
|
||||
try:
|
||||
if os.name == "nt":
|
||||
cmd = ["netstat", "-ano", "-p", "tcp"]
|
||||
out = subprocess.check_output(cmd, text=True)
|
||||
out = sp_check_output(["netstat", "-ano", "-p", "tcp"], text=True)
|
||||
for line in out.splitlines():
|
||||
if f"127.0.0.1:{port}" in line and "LISTENING" in line:
|
||||
return int(line.strip().split()[-1])
|
||||
else:
|
||||
cmd = ["lsof", "-t", f"-iTCP:{port}", "-sTCP:LISTEN"]
|
||||
out = subprocess.check_output(cmd, text=True)
|
||||
out = sp_check_output(["lsof", "-t", f"-iTCP:{port}", "-sTCP:LISTEN"], text=True)
|
||||
return int(out.strip().split()[0])
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _kill_pid_gracefully(self, pid: int):
|
||||
"""先 terminate 再 kill -9"""
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
time.sleep(1)
|
||||
@@ -375,7 +377,6 @@ class Deviceinfo(object):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _terminate_proc(self, p: Optional[subprocess.Popen]):
|
||||
if not p or p.poll() is not None:
|
||||
return
|
||||
@@ -404,4 +405,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]}")
|
||||
|
||||
@@ -266,14 +266,16 @@ def watchLiveForGrowth():
|
||||
def stopScript():
|
||||
body = request.get_json()
|
||||
udid = body.get("udid")
|
||||
code, massage = ThreadManager.stop(udid)
|
||||
return ResultData(code=code, data="", massage=massage).toJson()
|
||||
LogManager.method_info(f"接口收到 /stopScript udid={udid}", method="task")
|
||||
code, msg = ThreadManager.stop(udid)
|
||||
return ResultData(code=code, data="", massage=msg).toJson()
|
||||
|
||||
|
||||
# 关注打招呼
|
||||
@app.route('/passAnchorData', methods=['POST'])
|
||||
def passAnchorData():
|
||||
try:
|
||||
LogManager.method_info("关注打招呼","关注打招呼")
|
||||
data: Dict[str, Any] = request.get_json()
|
||||
# 设备列表
|
||||
idList = data.get("deviceList", [])
|
||||
@@ -431,7 +433,6 @@ def aiConfig():
|
||||
@app.route("/select_last_message", methods=['GET'])
|
||||
def select_last_message():
|
||||
data = JsonUtils.query_all_json_items()
|
||||
|
||||
return ResultData(data=data).toJson()
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import time
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union, Dict, List
|
||||
|
||||
import psutil
|
||||
|
||||
from Utils.LogManager import LogManager
|
||||
|
||||
|
||||
@@ -28,9 +30,39 @@ class FlaskSubprocessManager:
|
||||
self.comm_port = 34566
|
||||
self._stop_event = threading.Event()
|
||||
self._monitor_thread: Optional[threading.Thread] = None
|
||||
# 新增:启动前先把可能残留的 Flask 干掉
|
||||
self._kill_orphan_flask()
|
||||
atexit.register(self.stop)
|
||||
LogManager.info("FlaskSubprocessManager 单例已初始化", udid="system")
|
||||
|
||||
def _kill_orphan_flask(self):
|
||||
"""根据端口 34566 把遗留进程全部杀掉"""
|
||||
try:
|
||||
if os.name == "nt":
|
||||
# Windows
|
||||
out = subprocess.check_output(
|
||||
["netstat", "-ano"],
|
||||
text=True, startupinfo=self._si
|
||||
)
|
||||
for line in out.splitlines():
|
||||
if f"127.0.0.1:{self.comm_port}" in line and "LISTENING" in line:
|
||||
pid = int(line.strip().split()[-1])
|
||||
if pid != os.getpid():
|
||||
subprocess.run(["taskkill", "/F", "/PID", str(pid)],
|
||||
startupinfo=self._si,
|
||||
capture_output=True)
|
||||
else:
|
||||
# macOS / Linux
|
||||
out = subprocess.check_output(
|
||||
["lsof", "-t", f"-iTCP:{self.comm_port}", "-sTCP:LISTEN"],
|
||||
text=True
|
||||
)
|
||||
for pid in map(int, out.split()):
|
||||
if pid != os.getpid():
|
||||
os.kill(pid, 9)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ---------- 启动 ----------
|
||||
def start(self):
|
||||
with self._lock:
|
||||
@@ -108,27 +140,24 @@ class FlaskSubprocessManager:
|
||||
# ---------- 停止 ----------
|
||||
def stop(self):
|
||||
with self._lock:
|
||||
if getattr(self, 'process', None) is None:
|
||||
LogManager.info("无子进程需要停止", udid="system")
|
||||
if not self.process:
|
||||
return
|
||||
|
||||
pid = self.process.pid
|
||||
LogManager.info(f"正在停止 Flask 子进程 PID={pid}", udid="system")
|
||||
try:
|
||||
self.process.terminate()
|
||||
try:
|
||||
self.process.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
LogManager.warning("软杀超时,强制杀进程树", udid="system")
|
||||
import psutil
|
||||
parent = psutil.Process(pid)
|
||||
for child in parent.children(recursive=True):
|
||||
child.kill()
|
||||
parent.kill()
|
||||
self.process.wait()
|
||||
LogManager.info("Flask 子进程已停止", udid="system")
|
||||
# 1. 杀整棵树(Windows 也适用)
|
||||
parent = psutil.Process(pid)
|
||||
for child in parent.children(recursive=True):
|
||||
child.kill()
|
||||
parent.kill()
|
||||
gone, alive = psutil.wait_procs([parent] + parent.children(), timeout=3)
|
||||
for p in alive:
|
||||
p.kill() # 保险再补一刀
|
||||
self.process.wait()
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
except Exception as e:
|
||||
LogManager.error(f"停止子进程时异常:{e}", udid="system")
|
||||
LogManager.error(f"停止子进程异常:{e}", udid="system")
|
||||
finally:
|
||||
self.process = None
|
||||
self._stop_event.set()
|
||||
|
||||
BIN
Module/__pycache__/DeviceInfo.cpython-312.pyc
Normal file
BIN
Module/__pycache__/DeviceInfo.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Module/__pycache__/FlaskService.cpython-312.pyc
Normal file
BIN
Module/__pycache__/FlaskService.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Module/__pycache__/FlaskSubprocessManager.cpython-312.pyc
Normal file
BIN
Module/__pycache__/FlaskSubprocessManager.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Module/__pycache__/Main.cpython-312.pyc
Normal file
BIN
Module/__pycache__/Main.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,217 +1,3 @@
|
||||
#
|
||||
# import datetime
|
||||
# import io
|
||||
# import logging
|
||||
# import os
|
||||
# import re
|
||||
# import sys
|
||||
# import shutil
|
||||
# import zipfile
|
||||
# from pathlib import Path
|
||||
# import requests
|
||||
#
|
||||
#
|
||||
# class LogManager:
|
||||
# # 运行根目录:打包后取 exe 目录;源码运行取项目目录
|
||||
# if getattr(sys, "frozen", False):
|
||||
# projectRoot = os.path.dirname(sys.executable)
|
||||
# else:
|
||||
# projectRoot = os.path.dirname(os.path.dirname(__file__))
|
||||
#
|
||||
# logDir = os.path.join(projectRoot, "log")
|
||||
# _loggers = {}
|
||||
# _method_loggers = {} # 新增:缓存“设备+方法”的 logger
|
||||
#
|
||||
# # ---------- 工具函数 ----------
|
||||
# @classmethod
|
||||
# def _safe_filename(cls, name: str, max_len: int = 80) -> str:
|
||||
# """
|
||||
# 将方法名/udid等转成安全文件名:
|
||||
# - 允许字母数字、点、下划线、连字符
|
||||
# - 允许常见 CJK 字符(中日韩)
|
||||
# - 其他非法字符替换为下划线
|
||||
# - 合并多余下划线,裁剪长度
|
||||
# """
|
||||
# if not name:
|
||||
# return "unknown"
|
||||
# name = str(name).strip()
|
||||
#
|
||||
# # 替换 Windows 非法字符和控制符
|
||||
# name = re.sub(r'[\\/:*?"<>|\r\n\t]+', '_', name)
|
||||
#
|
||||
# # 只保留 ① 英数._- ② CJK 统一表意文字、日文平/片假名、韩文音节
|
||||
# name = re.sub(rf'[^a-zA-Z0-9_.\-'
|
||||
# r'\u4e00-\u9fff' # 中
|
||||
# r'\u3040-\u30ff' # 日
|
||||
# r'\uac00-\ud7a3' # 韩
|
||||
# r']+', '_', name)
|
||||
# # 合并多余下划线,去两端空白与下划线
|
||||
# name = re.sub(r'_+', '_', name).strip(' _.')
|
||||
# # 避免空
|
||||
# name = name or "unknown"
|
||||
# # Windows 预留名避免(CON/PRN/AUX/NUL/COM1…)
|
||||
# if re.fullmatch(r'(?i)(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])', name):
|
||||
# name = f"_{name}"
|
||||
# # 限长
|
||||
# return name[:max_len] or "unknown"
|
||||
#
|
||||
# # ---------- 旧的:按级别写固定文件 ----------
|
||||
# @classmethod
|
||||
# def _setupLogger(cls, udid, name, logName, level=logging.INFO):
|
||||
# """创建或获取 logger,并绑定到设备目录下的固定文件(info.log / warning.log / error.log)"""
|
||||
# deviceLogDir = os.path.join(cls.logDir, cls._safe_filename(udid))
|
||||
# os.makedirs(deviceLogDir, exist_ok=True)
|
||||
# logFile = os.path.join(deviceLogDir, logName)
|
||||
#
|
||||
# logger_name = f"{udid}_{name}"
|
||||
# logger = logging.getLogger(logger_name)
|
||||
# logger.setLevel(level)
|
||||
#
|
||||
# # 避免重复添加 handler
|
||||
# if not any(
|
||||
# isinstance(h, logging.FileHandler) and h.baseFilename == os.path.abspath(logFile)
|
||||
# for h in logger.handlers
|
||||
# ):
|
||||
# fileHandler = logging.FileHandler(logFile, mode="a", encoding="utf-8")
|
||||
# formatter = logging.Formatter(
|
||||
# "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
# datefmt="%Y-%m-%d %H:%M:%S"
|
||||
# )
|
||||
# fileHandler.setFormatter(formatter)
|
||||
# logger.addHandler(fileHandler)
|
||||
#
|
||||
# return logger
|
||||
#
|
||||
# @classmethod
|
||||
# def info(cls, text, udid="system"):
|
||||
# cls._setupLogger(udid, "infoLogger", "info.log", level=logging.INFO).info(f"[{udid}] {text}")
|
||||
#
|
||||
# @classmethod
|
||||
# def warning(cls, text, udid="system"):
|
||||
# cls._setupLogger(udid, "warningLogger", "warning.log", level=logging.WARNING).warning(f"[{udid}] {text}")
|
||||
#
|
||||
# @classmethod
|
||||
# def error(cls, text, udid="system"):
|
||||
# cls._setupLogger(udid, "errorLogger", "error.log", level=logging.ERROR).error(f"[{udid}] {text}")
|
||||
#
|
||||
# # ---------- 新增:按“设备+方法”分别写独立日志文件 ----------
|
||||
# @classmethod
|
||||
# def _setupMethodLogger(cls, udid: str, method: str, level=logging.INFO):
|
||||
# """
|
||||
# 为某设备的某个方法单独创建 logger:
|
||||
# log/<udid>/<method>.log
|
||||
# """
|
||||
# udid_key = cls._safe_filename(udid or "system")
|
||||
# method_key = cls._safe_filename(method or "general")
|
||||
# cache_key = (udid_key, method_key)
|
||||
#
|
||||
# # 命中缓存
|
||||
# if cache_key in cls._method_loggers:
|
||||
# return cls._method_loggers[cache_key]
|
||||
#
|
||||
# deviceLogDir = os.path.join(cls.logDir, udid_key)
|
||||
# os.makedirs(deviceLogDir, exist_ok=True)
|
||||
# logFile = os.path.join(deviceLogDir, f"{method_key}.log")
|
||||
#
|
||||
# logger_name = f"{udid_key}.{method_key}"
|
||||
# logger = logging.getLogger(logger_name)
|
||||
# logger.setLevel(level)
|
||||
# logger.propagate = False # 避免向根 logger 传播导致控制台重复打印
|
||||
#
|
||||
# # 避免重复添加 handler
|
||||
# if not any(
|
||||
# isinstance(h, logging.FileHandler) and h.baseFilename == os.path.abspath(logFile)
|
||||
# for h in logger.handlers
|
||||
# ):
|
||||
# fileHandler = logging.FileHandler(logFile, mode="a", encoding="utf-8")
|
||||
# formatter = logging.Formatter(
|
||||
# "%(asctime)s - %(levelname)s - %(name)s - %(message)s",
|
||||
# datefmt="%Y-%m-%d %H:%M:%S"
|
||||
# )
|
||||
# fileHandler.setFormatter(formatter)
|
||||
# logger.addHandler(fileHandler)
|
||||
#
|
||||
# cls._method_loggers[cache_key] = logger
|
||||
# return logger
|
||||
#
|
||||
# @classmethod
|
||||
# def method_info(cls, text, method, udid="system"):
|
||||
# """按设备+方法写 INFO 到 log/<udid>/<method>.log"""
|
||||
# cls._setupMethodLogger(udid, method, level=logging.INFO).info(f"[{udid}][{method}] {text}")
|
||||
#
|
||||
# @classmethod
|
||||
# def method_warning(cls, text, method, udid="system"):
|
||||
# cls._setupMethodLogger(udid, method, level=logging.WARNING).warning(f"[{udid}][{method}] {text}")
|
||||
#
|
||||
# @classmethod
|
||||
# def method_error(cls, text, method, udid="system"):
|
||||
# cls._setupMethodLogger(udid, method, level=logging.ERROR).error(f"[{udid}][{method}] {text}")
|
||||
#
|
||||
# # 清空日志
|
||||
# @classmethod
|
||||
# def clearLogs(cls):
|
||||
# """启动时清空 log 目录下所有文件"""
|
||||
#
|
||||
# # 关闭所有 handler
|
||||
# for name, logger in logging.Logger.manager.loggerDict.items():
|
||||
# if isinstance(logger, logging.Logger):
|
||||
# for handler in logger.handlers[:]:
|
||||
# try:
|
||||
# handler.close()
|
||||
# except Exception:
|
||||
# pass
|
||||
# logger.removeHandler(handler)
|
||||
#
|
||||
# # 删除 log 目录
|
||||
# log_path = Path(cls.logDir)
|
||||
# if log_path.exists():
|
||||
# for item in log_path.iterdir():
|
||||
# if item.is_file():
|
||||
# item.unlink()
|
||||
# elif item.is_dir():
|
||||
# shutil.rmtree(item)
|
||||
#
|
||||
# # 清缓存
|
||||
# cls._method_loggers.clear()
|
||||
#
|
||||
# @classmethod
|
||||
# def upload_all_logs(cls, server_url, token, userId, tenantId):
|
||||
# log_path = Path(cls.logDir)
|
||||
# if not log_path.exists():
|
||||
# return False
|
||||
#
|
||||
# timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
# filename = f"{timestamp}_logs.zip"
|
||||
# print(filename)
|
||||
# zip_buf = io.BytesIO()
|
||||
# with zipfile.ZipFile(zip_buf, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
# for p in log_path.rglob("*"):
|
||||
# if p.is_file():
|
||||
# arcname = str(p.relative_to(log_path))
|
||||
# zf.write(p, arcname=arcname)
|
||||
#
|
||||
# zip_bytes = zip_buf.getvalue()
|
||||
#
|
||||
# headers = {"vvtoken": token}
|
||||
# data = {"tenantId": tenantId, "userId": userId}
|
||||
#
|
||||
#
|
||||
# files = {
|
||||
# "file": (filename, io.BytesIO(zip_bytes), "application/zip")
|
||||
# }
|
||||
#
|
||||
# # 3) 上传
|
||||
# resp = requests.post(server_url, headers=headers, data=data, files=files)
|
||||
# if resp.json()['data']:
|
||||
# return True
|
||||
# return False
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import io
|
||||
|
||||
24
Utils/SubprocessKit.py
Normal file
24
Utils/SubprocessKit.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
__all__ = ['check_output', 'popen', 'PIPE']
|
||||
|
||||
# 模块级单例,导入时只创建一次
|
||||
if os.name == "nt":
|
||||
_si = subprocess.STARTUPINFO()
|
||||
_si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
_si.wShowWindow = subprocess.SW_HIDE
|
||||
else:
|
||||
_si = None
|
||||
|
||||
PIPE = subprocess.PIPE
|
||||
|
||||
def check_output(cmd, **kw):
|
||||
if os.name == "nt":
|
||||
kw.setdefault('startupinfo', _si)
|
||||
return subprocess.check_output(cmd, **kw)
|
||||
|
||||
def popen(*args, **kw):
|
||||
if os.name == "nt":
|
||||
kw.setdefault('startupinfo', _si)
|
||||
return subprocess.Popen(*args, **kw)
|
||||
@@ -1,33 +1,126 @@
|
||||
from threading import Thread, Event
|
||||
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 Utils.LogManager import LogManager
|
||||
from script.ScriptManager import ScriptManager
|
||||
|
||||
|
||||
class ThreadManager():
|
||||
threads = {}
|
||||
class ThreadManager:
|
||||
"""
|
||||
对调用方完全透明:
|
||||
add(udid, thread_obj, stop_event) 保持原签名
|
||||
stop(udid) 保持原签名
|
||||
但内部把 thread_obj 当成“壳”,真正拉起的是子进程。
|
||||
"""
|
||||
_pool: Dict[str, psutil.Process] = {}
|
||||
_lock = threading.Lock()
|
||||
|
||||
@classmethod
|
||||
def add(cls, udid, t: Thread, stopEvent: Event):
|
||||
if udid in cls.threads:
|
||||
print("▲ 线程已存在")
|
||||
return
|
||||
cls.threads[udid] = {"thread": t, "stopEvent": stopEvent}
|
||||
|
||||
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")
|
||||
|
||||
@classmethod
|
||||
def stop(cls, udid):
|
||||
try:
|
||||
info = cls.threads[udid]
|
||||
if info:
|
||||
info["stopEvent"].set() # 停止线程
|
||||
info["thread"].join(timeout=3) # 等待线程退出
|
||||
del cls.threads[udid]
|
||||
LogManager.info("停止线程成功", udid)
|
||||
return 200, "停止线程成功 " + udid
|
||||
else:
|
||||
LogManager.info("无此线程,无需关闭", udid)
|
||||
return 1001, "无此线程,无需关闭 " + udid
|
||||
except KeyError as e:
|
||||
LogManager.info("无此线程,无需关闭", udid)
|
||||
return 1001, "停止脚本失败 " + udid
|
||||
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:
|
||||
"""
|
||||
真正拉起子进程:
|
||||
打包环境:exe --udid=xxx
|
||||
源码环境:python -m Module.Worker --udid=xxx
|
||||
"""
|
||||
exe_path = Path(sys.executable).resolve()
|
||||
is_frozen = exe_path.suffix.lower() == ".exe" and exe_path.exists()
|
||||
|
||||
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)
|
||||
|
||||
# 核心: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)
|
||||
|
||||
@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()
|
||||
BIN
Utils/__pycache__/AiUtils.cpython-312.pyc
Normal file
BIN
Utils/__pycache__/AiUtils.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Utils/__pycache__/ControlUtils.cpython-312.pyc
Normal file
BIN
Utils/__pycache__/ControlUtils.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Utils/__pycache__/DevDiskImageDeployer.cpython-312.pyc
Normal file
BIN
Utils/__pycache__/DevDiskImageDeployer.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Utils/__pycache__/JsonUtils.cpython-312.pyc
Normal file
BIN
Utils/__pycache__/JsonUtils.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Utils/__pycache__/LogManager.cpython-312.pyc
Normal file
BIN
Utils/__pycache__/LogManager.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Utils/__pycache__/Requester.cpython-312.pyc
Normal file
BIN
Utils/__pycache__/Requester.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Utils/__pycache__/SubprocessKit.cpython-312.pyc
Normal file
BIN
Utils/__pycache__/SubprocessKit.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Utils/__pycache__/ThreadManager.cpython-312.pyc
Normal file
BIN
Utils/__pycache__/ThreadManager.cpython-312.pyc
Normal file
Binary file not shown.
12
build.bat
12
build.bat
@@ -1,9 +1,9 @@
|
||||
python -m nuitka "Module/Main.py" ^
|
||||
python -m nuitka "C:\Users\zhangkai\Desktop\20250916ios\iOSAI\Module\Main.py" ^
|
||||
--standalone ^
|
||||
--msvc=latest ^
|
||||
--windows-console-mode=disable ^
|
||||
--remove-output ^
|
||||
--output-dir="F:/company code/AI item/20250820/iOSAI/out" ^
|
||||
--output-dir="C:\Users\zhangkai\Desktop\20250916ios\iOSAI\out" ^
|
||||
--output-filename=IOSAI ^
|
||||
--include-package=Module,Utils,Entity,script ^
|
||||
--include-module=flask ^
|
||||
@@ -18,7 +18,7 @@ python -m nuitka "Module/Main.py" ^
|
||||
--include-module=urllib3 ^
|
||||
--include-module=certifi ^
|
||||
--include-module=idna ^
|
||||
--include-data-dir="F:/company code/AI item/20250820/iOSAI/SupportFiles=SupportFiles" ^
|
||||
--include-data-dir="F:/company code/AI item/20250820/iOSAI/resources=resources" ^
|
||||
--include-data-files="F:/company code/AI item/20250820/iOSAI/resources/iproxy/*=resources/iproxy/" ^
|
||||
--windows-icon-from-ico="F:/company code/AI item/20250820/iOSAI/resources/icon.ico"
|
||||
--include-data-dir="C:\Users\zhangkai\Desktop\20250916ios\iOSAI\SupportFiles=SupportFiles" ^
|
||||
--include-data-dir="C:\Users\zhangkai\Desktop\20250916ios\iOSAI\resources=resources" ^
|
||||
--include-data-files="C:\Users\zhangkai\Desktop\20250916ios\iOSAI\resources\iproxy\*=resources/iproxy/" ^
|
||||
--windows-icon-from-ico="C:\Users\zhangkai\Desktop\20250916ios\iOSAI\resources\icon.ico"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -361,10 +361,12 @@ class ScriptManager():
|
||||
else:
|
||||
print(f"找不到输入框")
|
||||
|
||||
input.clear_text()
|
||||
time.sleep(1)
|
||||
# 输入主播id
|
||||
input.set_text(f"{aid or '暂无数据'}\n")
|
||||
input = session.xpath('//XCUIElementTypeSearchField')
|
||||
if input.exists:
|
||||
input.clear_text()
|
||||
time.sleep(1)
|
||||
# 输入主播id
|
||||
input.set_text(f"{aid or '暂无数据'}\n")
|
||||
|
||||
# 定位 "关注" 按钮 通过关注按钮的位置点击主播首页
|
||||
|
||||
@@ -449,11 +451,11 @@ class ScriptManager():
|
||||
time.sleep(2)
|
||||
msgButton = AiUtils.getSendMesageButton(session)
|
||||
time.sleep(2)
|
||||
if msgButton is not None:
|
||||
LogManager.method_info("找到发消息按钮了", "关注打招呼", udid)
|
||||
print("找到发消息按钮了")
|
||||
if msgButton.exists:
|
||||
# 进入聊天页面
|
||||
msgButton.click()
|
||||
LogManager.method_info("找到发消息按钮了", "关注打招呼", udid)
|
||||
print("找到发消息按钮了")
|
||||
else:
|
||||
LogManager.method_info("没有识别出发消息按钮", "关注打招呼", udid)
|
||||
print("没有识别出发消息按钮")
|
||||
@@ -492,9 +494,9 @@ class ScriptManager():
|
||||
else:
|
||||
msg = text
|
||||
LogManager.method_info(f"即将发送的私信内容:{msg}", "关注打招呼", udid)
|
||||
chatInput = session.xpath("//TextView")
|
||||
|
||||
# 准备发送一条信息
|
||||
chatInput = session.xpath("//TextView")
|
||||
if chatInput.exists:
|
||||
chatInput.click()
|
||||
time.sleep(2)
|
||||
|
||||
BIN
script/__pycache__/ScriptManager.cpython-312.pyc
Normal file
BIN
script/__pycache__/ScriptManager.cpython-312.pyc
Normal file
Binary file not shown.
45
tidevice.spec
Normal file
45
tidevice.spec
Normal file
@@ -0,0 +1,45 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
from PyInstaller.utils.hooks import collect_all
|
||||
|
||||
datas = [('C:\\Users\\milk\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\tidevice', 'tidevice')]
|
||||
binaries = []
|
||||
hiddenimports = ['tidevice._proto', 'tidevice._instruments', 'tidevice._usbmux', 'tidevice._wdaproxy']
|
||||
tmp_ret = collect_all('tidevice')
|
||||
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['tidevice_entry.py'],
|
||||
pathex=[],
|
||||
binaries=binaries,
|
||||
datas=datas,
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name='tidevice',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
Reference in New Issue
Block a user