Compare commits
2 Commits
da2ac45177
...
26fc6bb8e6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26fc6bb8e6 | ||
|
|
690b17ec58 |
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@@ -1,8 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
6
.idea/git_toolbox_blame.xml
generated
6
.idea/git_toolbox_blame.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitToolBoxBlameSettings">
|
||||
<option name="version" value="2" />
|
||||
</component>
|
||||
</project>
|
||||
15
.idea/git_toolbox_prj.xml
generated
15
.idea/git_toolbox_prj.xml
generated
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitToolBoxProjectSettings">
|
||||
<option name="commitMessageIssueKeyValidationOverride">
|
||||
<BoolValueOverride>
|
||||
<option name="enabled" value="true" />
|
||||
</BoolValueOverride>
|
||||
</option>
|
||||
<option name="commitMessageValidationEnabledOverride">
|
||||
<BoolValueOverride>
|
||||
<option name="enabled" value="true" />
|
||||
</BoolValueOverride>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/iOSAI.iml
generated
6
.idea/iOSAI.iml
generated
@@ -1,10 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.12 (IOS-AI)" jdkType="Python SDK" />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.12" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
5
.idea/inspectionProfiles/Project_Default.xml
generated
5
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -4,8 +4,11 @@
|
||||
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoredPackages">
|
||||
<value>
|
||||
<list size="1">
|
||||
<list size="4">
|
||||
<item index="0" class="java.lang.String" itemvalue="PySide6" />
|
||||
<item index="1" class="java.lang.String" itemvalue="pyusb" />
|
||||
<item index="2" class="java.lang.String" itemvalue="PyGObject-stubs" />
|
||||
<item index="3" class="java.lang.String" itemvalue="PyGObject" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
|
||||
5
.idea/misc.xml
generated
5
.idea/misc.xml
generated
@@ -1,7 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.12 (iOSAI)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (IOS-AI)" project-jdk-type="Python SDK" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
118
.idea/workspace.xml
generated
Normal file
118
.idea/workspace.xml
generated
Normal file
@@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/.gitignore" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/git_toolbox_blame.xml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/git_toolbox_prj.xml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/iOSAI.iml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/inspectionProfiles/Project_Default.xml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/inspectionProfiles/profiles_settings.xml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/modules.xml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Module/FlaskService.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/FlaskService.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Utils/AiUtils.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/AiUtils.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/IOSAI.exe" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/certifi/cacert.pem" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/cv2/opencv_videoio_ffmpeg4120_64.dll" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/jaraco/text/Lorem ipsum.txt" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/libcrypto-3.dll" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/libffi-8.dll" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/libssl-3.dll" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/numpy.libs/libscipy_openblas64_-43e11ff0749b8cbe0a615c9cf6737e0e.dll" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/numpy.libs/msvcp140-263139962577ecda4cd9469ca360a746.dll" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/python3.dll" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/python312.dll" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/pythoncom312.dll" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/pywintypes312.dll" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/resources/add.png" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/resources/advertisement.png" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/resources/back.png" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/resources/comment.png" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/resources/icon.ico" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/resources/like.png" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/resources/search.png" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/tcl86t.dll" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/tk86t.dll" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/vcruntime140.dll" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/vcruntime140_1.dll" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/out/Main.dist/zlib1.dll" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/script/ScriptManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/script/ScriptManager.py" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo"><![CDATA[{
|
||||
"customColor": "",
|
||||
"associatedIndex": 5
|
||||
}]]></component>
|
||||
<component name="ProjectId" id="31K5VnrGt5SBVafKVv8gi0HPOF6" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"git-widget-placeholder": "Merging main",
|
||||
"last_opened_file_path": "E:/Code/python/iOSAI/Module/Main.py",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}]]></component>
|
||||
<component name="RunManager">
|
||||
<configuration name="Main" type="PythonConfigurationType" factoryName="Python" 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="SDK_NAME" value="Python 3.12" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="IS_MODULE_SDK" value="false" />
|
||||
<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$/Module/Main.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>
|
||||
</component>
|
||||
<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" />
|
||||
</set>
|
||||
</attachedChunks>
|
||||
</component>
|
||||
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="" />
|
||||
<created>1755259950275</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1755259950275</updated>
|
||||
<workItem from="1755259951484" duration="38000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,111 +1,199 @@
|
||||
import subprocess
|
||||
import threading
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import wda
|
||||
import threading
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
from tidevice import Usbmux
|
||||
from Entity.DeviceModel import DeviceModel
|
||||
from Entity.Variables import WdaAppBundleId
|
||||
from Module.FlaskSubprocessManager import FlaskSubprocessManager
|
||||
from Utils.LogManager import LogManager
|
||||
|
||||
threadLock = threading.Lock()
|
||||
|
||||
class Deviceinfo(object):
|
||||
def __init__(self):
|
||||
self.deviceIndex = 0
|
||||
# 投屏端口
|
||||
# 投屏端口(本地映射端口起始值,会递增)
|
||||
self.screenProxy = 9110
|
||||
# 存放pid的数组
|
||||
self.pidList = []
|
||||
# 设备列表
|
||||
self.deviceArray = []
|
||||
# 获取到县城管理类
|
||||
self.manager = FlaskSubprocessManager.get_instance()
|
||||
# 给前端的设备模型数组
|
||||
self.deviceModelList = []
|
||||
|
||||
# 监听设备连接
|
||||
# 记录 iproxy Popen 进程:[{ "id": udid, "target": Popen }, ...]
|
||||
self.pidList: List[Dict] = []
|
||||
# 当前已连接的设备(tidevice 的 Device 对象列表)
|
||||
self.deviceArray: List = []
|
||||
|
||||
# 子进程通信(向前端发送设备信息)
|
||||
self.manager = FlaskSubprocessManager.get_instance()
|
||||
# 已发给前端的设备模型列表(用于拔出时发 type=2)
|
||||
self.deviceModelList: List[DeviceModel] = []
|
||||
|
||||
# ----------------------------
|
||||
# 监听设备连接(死循环,内部捕获异常)
|
||||
# ----------------------------
|
||||
def startDeviceListener(self):
|
||||
LogManager.info("Device Listener started", "listener")
|
||||
while True:
|
||||
lists = Usbmux().device_list()
|
||||
# 添加设备逻辑
|
||||
try:
|
||||
lists = Usbmux().device_list()
|
||||
except Exception as e:
|
||||
# 另一台电脑常见:usbmuxd 连接失败(未安装 iTunes/Apple Mobile Device Support)
|
||||
LogManager.warning(f"usbmuxd 连接失败: {e}。请确认已安装 iTunes/Apple Mobile Device Support,并在手机上“信任此电脑”", "listener")
|
||||
time.sleep(2)
|
||||
continue
|
||||
|
||||
# 新接入设备
|
||||
for device in lists:
|
||||
if device not in self.deviceArray:
|
||||
self.screenProxy += 1
|
||||
self.connectDevice(device.udid)
|
||||
self.deviceArray.append(device)
|
||||
try:
|
||||
self.connectDevice(device.udid)
|
||||
self.deviceArray.append(device)
|
||||
except Exception as e:
|
||||
LogManager.error(f"连接设备失败 {device.udid}: {e}", device.udid)
|
||||
|
||||
# 处理拔出设备的逻辑
|
||||
def removeDevice():
|
||||
set1 = set(self.deviceArray)
|
||||
set2 = set(lists)
|
||||
difference = set1 - set2
|
||||
differenceList = list(difference)
|
||||
for i in differenceList:
|
||||
for j in self.deviceArray:
|
||||
# 判断是否为差异设备
|
||||
if i.udid == j.udid:
|
||||
# 从设备模型中删除数据
|
||||
for a in self.deviceModelList:
|
||||
if i.udid == a.deviceId:
|
||||
a.type = 2
|
||||
# 发送数据
|
||||
self.manager.send(a.toDict())
|
||||
self.deviceModelList.remove(a)
|
||||
# 拔出设备处理
|
||||
self._removeDisconnected(lists)
|
||||
|
||||
for k in self.pidList:
|
||||
# 干掉端口短发进程
|
||||
if j.udid == k["id"]:
|
||||
target = k["target"]
|
||||
target.kill()
|
||||
self.pidList.remove(k)
|
||||
# 删除已经拔出的设备
|
||||
self.deviceArray.remove(j)
|
||||
|
||||
removeDevice()
|
||||
time.sleep(1)
|
||||
|
||||
# 连接设备
|
||||
def connectDevice(self, identifier):
|
||||
# ----------------------------
|
||||
# 连接单台设备:启动 WDA、读取屏参、通知前端、映射投屏端口
|
||||
# ----------------------------
|
||||
def connectDevice(self, identifier: str):
|
||||
# 1) 连接 WDA(USBClient -> 设备 8100)
|
||||
try:
|
||||
d = wda.USBClient(identifier, 8100)
|
||||
LogManager.info("启动wda成功", identifier)
|
||||
|
||||
size = d.window_size()
|
||||
width = size.width
|
||||
height = size.height
|
||||
scale = d.scale
|
||||
# 创建模型
|
||||
model = DeviceModel(identifier, self.screenProxy, width, height, scale, type=1)
|
||||
self.deviceModelList.append(model)
|
||||
# 发送数据
|
||||
self.manager.send(model.toDict())
|
||||
|
||||
LogManager.info("启动 WDA 成功", identifier)
|
||||
except Exception as e:
|
||||
LogManager.error("启动wda失败。请检查wda是否正常", identifier)
|
||||
return
|
||||
LogManager.error(f"启动 WDA 失败,请检查手机是否已信任、WDA 是否正常。错误: {e}", identifier)
|
||||
return # 不抛出到外层,保持监听循环健壮
|
||||
|
||||
d.app_start(WdaAppBundleId)
|
||||
d.home()
|
||||
time.sleep(2)
|
||||
target = self.relayDeviceScreenPort(identifier)
|
||||
self.pidList.append({
|
||||
"target": target,
|
||||
"id": identifier
|
||||
})
|
||||
|
||||
|
||||
|
||||
# 转发设备端口
|
||||
def relayDeviceScreenPort(self, udid):
|
||||
# 2) 读取屏幕信息(失败不影响主流程)
|
||||
width, height, scale = 0, 0, 1.0
|
||||
try:
|
||||
command = f"iproxy.exe -u {udid} {self.screenProxy} 9100"
|
||||
# 创建一个没有窗口的进程
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
startupinfo.wShowWindow = 0
|
||||
r = subprocess.Popen(command, shell=True, startupinfo=startupinfo)
|
||||
return r
|
||||
size = d.window_size()
|
||||
width, height = size.width, size.height
|
||||
scale = d.scale
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return 0
|
||||
LogManager.warning(f"读取屏幕信息失败:{e}", identifier)
|
||||
|
||||
# 3) 组装模型并发送给前端
|
||||
model = DeviceModel(identifier, self.screenProxy, width, height, scale, type=1)
|
||||
self.deviceModelList.append(model)
|
||||
try:
|
||||
self.manager.send(model.toDict())
|
||||
except Exception as e:
|
||||
LogManager.warning(f"向前端发送设备模型失败:{e}", identifier)
|
||||
|
||||
# 4) 可选:启动你的 app 并回到桌面
|
||||
try:
|
||||
d.app_start(WdaAppBundleId)
|
||||
d.home()
|
||||
except Exception as e:
|
||||
LogManager.warning(f"启动/切回桌面失败:{e}", identifier)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# 5) 本地端口 -> 设备端口 的映射(投屏:本地 self.screenProxy -> 设备 9100)
|
||||
target = self.relayDeviceScreenPort(identifier)
|
||||
self.pidList.append({"target": target, "id": identifier})
|
||||
|
||||
# ----------------------------
|
||||
# 处理拔出设备:发通知、关掉 iproxy、移出状态
|
||||
# ----------------------------
|
||||
def _removeDisconnected(self, current_list):
|
||||
set1 = set(self.deviceArray)
|
||||
set2 = set(current_list)
|
||||
difference = list(set1 - set2) # 在旧集合中但不在新集合中 -> 已拔出
|
||||
|
||||
for i in difference:
|
||||
udid = i.udid
|
||||
# 1) 通知前端:type = 2
|
||||
for a in list(self.deviceModelList):
|
||||
if udid == a.deviceId:
|
||||
a.type = 2
|
||||
try:
|
||||
self.manager.send(a.toDict())
|
||||
except Exception as e:
|
||||
LogManager.warning(f"发送下线事件失败:{e}", udid)
|
||||
self.deviceModelList.remove(a)
|
||||
|
||||
# 2) 关掉对应的 iproxy
|
||||
for k in list(self.pidList):
|
||||
if udid == k["id"]:
|
||||
target = k.get("target")
|
||||
try:
|
||||
if target and target.poll() is None:
|
||||
target.kill()
|
||||
except Exception:
|
||||
pass
|
||||
self.pidList.remove(k)
|
||||
|
||||
# 3) 从已连接集合中移除
|
||||
try:
|
||||
self.deviceArray.remove(i)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# 路径:打包/源码都能找到根目录、iproxy 目录和 iproxy 可执行文件
|
||||
# ----------------------------
|
||||
def _base_dir(self) -> Path:
|
||||
"""
|
||||
打包后:返回 exe 所在目录;
|
||||
源码运行:返回项目根目录(Module 的上一级)
|
||||
"""
|
||||
if getattr(sys, "frozen", False):
|
||||
return Path(sys.executable).resolve().parent
|
||||
return Path(__file__).resolve().parents[1] # iOSAI/ 作为根
|
||||
|
||||
def _iproxy_dir(self) -> Path:
|
||||
"""返回打包后的 iproxy 目录(你现在放在 Module/iproxy/)"""
|
||||
return self._base_dir() / "Module" / "iproxy"
|
||||
|
||||
def _iproxy_path(self) -> Path:
|
||||
"""返回 iproxy 可执行文件路径(Windows 为 iproxy.exe)"""
|
||||
exe_name = "iproxy.exe" if os.name == "nt" else "iproxy"
|
||||
return self._iproxy_dir() / exe_name
|
||||
|
||||
# ----------------------------
|
||||
# 端口映射:启动 iproxy(设置 cwd 和 PATH,隐藏窗口)
|
||||
# ----------------------------
|
||||
def relayDeviceScreenPort(self, udid: str) -> Optional[subprocess.Popen]:
|
||||
try:
|
||||
iproxy = self._iproxy_path()
|
||||
iproxy_dir = self._iproxy_dir()
|
||||
|
||||
if not iproxy.exists():
|
||||
raise FileNotFoundError(f"iproxy not found: {iproxy}")
|
||||
|
||||
# 继承环境并把 iproxy 目录加入 PATH,方便 DLL 解析
|
||||
env = os.environ.copy()
|
||||
env["PATH"] = str(iproxy_dir) + os.pathsep + env.get("PATH", "")
|
||||
|
||||
# Windows 隐藏子进程窗口
|
||||
CREATE_NO_WINDOW = 0x08000000 if os.name == "nt" else 0
|
||||
|
||||
p = subprocess.Popen(
|
||||
[str(iproxy), "-u", udid, str(self.screenProxy), "9100"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
creationflags=CREATE_NO_WINDOW,
|
||||
cwd=str(iproxy_dir), # 关键:工作目录设为 iproxy 所在目录
|
||||
env=env, # 关键:把 iproxy_dir 注入 PATH
|
||||
text=True, # 你后面如果要读 stdout/stderr 的话更方便
|
||||
encoding="utf-8",
|
||||
bufsize=1
|
||||
)
|
||||
LogManager.info(f"启动 iproxy 成功,本地 {self.screenProxy} -> 设备 9100", udid)
|
||||
return p
|
||||
|
||||
except Exception as e:
|
||||
LogManager.error(f"启动 iproxy 失败:{e}", udid)
|
||||
return None
|
||||
@@ -137,10 +137,8 @@ def tapAction():
|
||||
client = wda.USBClient(udid)
|
||||
session = client.session()
|
||||
session.appium_settings({"snapshotMaxDepth": 0})
|
||||
|
||||
x = body.get("x")
|
||||
y = body.get("y")
|
||||
|
||||
session.tap(x, y)
|
||||
return ResultData(data="").toJson()
|
||||
|
||||
@@ -6,6 +6,7 @@ import json
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union, Dict, List
|
||||
|
||||
class FlaskSubprocessManager:
|
||||
@@ -21,46 +22,59 @@ class FlaskSubprocessManager:
|
||||
|
||||
def _init_manager(self):
|
||||
self.process: Optional[subprocess.Popen] = None
|
||||
self.comm_port = self._find_available_port()
|
||||
self.comm_port = 34567
|
||||
self._stop_event = threading.Event()
|
||||
atexit.register(self.stop)
|
||||
|
||||
def _find_available_port(self):
|
||||
"""动态获取可用端口"""
|
||||
# 可以把 _find_available_port 留着备用,但 start 前先校验端口是否被占用
|
||||
def _is_port_busy(self, port: int) -> bool:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(('0.0.0.0', 0))
|
||||
return s.getsockname()[1]
|
||||
s.settimeout(0.2)
|
||||
return s.connect_ex(("127.0.0.1", port)) == 0
|
||||
|
||||
# 启动flask
|
||||
def start(self):
|
||||
"""启动子进程(Windows兼容方案)"""
|
||||
"""启动 Flask 子进程(兼容打包后的 exe 和源码运行)"""
|
||||
with self._lock:
|
||||
if self.process is not None:
|
||||
raise RuntimeError("子进程已在运行中!")
|
||||
# 通过环境变量传递通信端口
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__)) # 当前脚本所在路径
|
||||
script_path = os.path.abspath(os.path.join(base_dir, "../Flask/FlaskService.py"))
|
||||
python_executable = os.path.abspath(sys.executable) # 获取当前解释器路径
|
||||
|
||||
if not os.path.isfile(script_path):
|
||||
raise FileNotFoundError(f"❌ 找不到 FlaskService.py: {script_path}")
|
||||
|
||||
# 通过环境变量传递通信端口
|
||||
env = os.environ.copy()
|
||||
env['FLASK_COMM_PORT'] = str(self.comm_port)
|
||||
env["FLASK_COMM_PORT"] = str(self.comm_port)
|
||||
|
||||
# —— 解析打包 exe 的稳健写法 ——
|
||||
exe_path = Path(sys.executable).resolve()
|
||||
if exe_path.name.lower() in ("python.exe", "pythonw.exe"):
|
||||
# Nuitka 某些场景里 sys.executable 可能指向 dist\python.exe(并不存在)
|
||||
exe_path = Path(sys.argv[0]).resolve()
|
||||
|
||||
is_frozen = exe_path.suffix.lower() == ".exe" and exe_path.exists()
|
||||
|
||||
if is_frozen:
|
||||
# 打包后的 exe:用当前 exe 自举
|
||||
cmd = [str(exe_path), "--role=flask"]
|
||||
cwd = str(exe_path.parent)
|
||||
else:
|
||||
# 源码运行:模块方式更稳
|
||||
cmd = [sys.executable, "-m", "Module.Main", "--role=flask"]
|
||||
cwd = str(Path(__file__).resolve().parent) # Module 目录
|
||||
|
||||
print(f"[DEBUG] spawn: {cmd} (cwd={cwd}) exists(exe)={os.path.exists(cmd[0])}")
|
||||
|
||||
self.process = subprocess.Popen(
|
||||
[python_executable, script_path], # 启动一个子进程 FlaskService.py
|
||||
stdin=subprocess.PIPE, # 标准输入流,用于向子进程发送数据
|
||||
stdout=subprocess.PIPE, # 标准输出流,用于接收子进程的输出
|
||||
stderr=subprocess.PIPE, # 标准错误流,用于接收子进程的错误信息
|
||||
text=True, # 以文本模式打开流,否则以二进制模式打开
|
||||
bufsize=1, # 缓冲区大小设置为 1,表示行缓冲
|
||||
encoding='utf-8', # 指定编码为 UTF-8,确保控制台输出不会报错
|
||||
env=env # 指定子进程的环境变量
|
||||
cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace", # 新增:遇到非 UTF-8 字节用 <20> 代替,避免崩溃
|
||||
bufsize=1,
|
||||
env=env,
|
||||
cwd=cwd,
|
||||
)
|
||||
print(f"Flask子进程启动 (PID: {self.process.pid}, 通信端口: {self.comm_port})")
|
||||
print(f"Flask子进程启动 (PID: {self.process.pid}, 端口: {self.comm_port})")
|
||||
|
||||
# 将日志通过主进程输出
|
||||
def print_output(stream, stream_name):
|
||||
while True:
|
||||
line = stream.readline()
|
||||
@@ -68,7 +82,6 @@ class FlaskSubprocessManager:
|
||||
break
|
||||
print(f"{stream_name}: {line.strip()}")
|
||||
|
||||
# 启动两个线程分别处理 stdout 和 stderr
|
||||
threading.Thread(target=print_output, args=(self.process.stdout, "STDOUT"), daemon=True).start()
|
||||
threading.Thread(target=print_output, args=(self.process.stderr, "STDERR"), daemon=True).start()
|
||||
|
||||
|
||||
@@ -1,17 +1,66 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from Module.DeviceInfo import Deviceinfo
|
||||
from Module.FlaskSubprocessManager import FlaskSubprocessManager
|
||||
from Utils.LogManager import LogManager
|
||||
|
||||
# 项目入口
|
||||
if __name__ == "__main__":
|
||||
# 清空日志
|
||||
LogManager.clearLogs()
|
||||
time.sleep(1)
|
||||
# 确定 exe 或 py 文件所在目录
|
||||
BASE = Path(getattr(sys, 'frozen', False) and sys.executable or __file__).resolve().parent
|
||||
LOG_DIR = BASE / "log"
|
||||
LOG_DIR.mkdir(exist_ok=True) # 确保 log 目录存在
|
||||
|
||||
print(f"日志目录: {LOG_DIR}")
|
||||
|
||||
def _run_flask_role():
|
||||
from Module import FlaskService
|
||||
port = int(os.getenv("FLASK_COMM_PORT", "34567")) # 固定端口的兜底仍是 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, debug=False, use_reloader=False)
|
||||
|
||||
if "--role=flask" in sys.argv:
|
||||
_run_flask_role()
|
||||
sys.exit(0)
|
||||
|
||||
# 项目入口
|
||||
# ... 省略前面的 import 和函数 ...
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 清空日志等
|
||||
LogManager.clearLogs()
|
||||
|
||||
# 启动 Flask 子进程
|
||||
manager = FlaskSubprocessManager.get_instance()
|
||||
manager.start()
|
||||
|
||||
info = Deviceinfo()
|
||||
info.startDeviceListener()
|
||||
# 设备监听(即使失败/很快返回,也不会导致主进程退出)
|
||||
try:
|
||||
info = Deviceinfo()
|
||||
info.startDeviceListener()
|
||||
except Exception as e:
|
||||
print("[WARN] Device listener not running:", e)
|
||||
|
||||
# === 保活:阻塞主线程,直到收到 Ctrl+C/关闭 ===
|
||||
import threading, time, signal
|
||||
|
||||
stop = threading.Event()
|
||||
|
||||
def _handle(_sig, _frm):
|
||||
stop.set()
|
||||
|
||||
# Windows 上 SIGINT/SIGTERM 都可以拦到
|
||||
try:
|
||||
signal.signal(signal.SIGINT, _handle)
|
||||
signal.signal(signal.SIGTERM, _handle)
|
||||
except Exception:
|
||||
pass # 某些环境可能不支持,忽略
|
||||
|
||||
try:
|
||||
while not stop.is_set():
|
||||
time.sleep(1)
|
||||
finally:
|
||||
# 进程退出前记得把子进程关掉
|
||||
manager.stop()
|
||||
|
||||
BIN
Module/iproxy/bz2.dll
Normal file
BIN
Module/iproxy/bz2.dll
Normal file
Binary file not shown.
BIN
Module/iproxy/getopt.dll
Normal file
BIN
Module/iproxy/getopt.dll
Normal file
Binary file not shown.
BIN
Module/iproxy/idevice_id.exe
Normal file
BIN
Module/iproxy/idevice_id.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/idevicebackup.exe
Normal file
BIN
Module/iproxy/idevicebackup.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/idevicebackup2.exe
Normal file
BIN
Module/iproxy/idevicebackup2.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/idevicebtlogger.exe
Normal file
BIN
Module/iproxy/idevicebtlogger.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/idevicecrashreport.exe
Normal file
BIN
Module/iproxy/idevicecrashreport.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/idevicedate.exe
Normal file
BIN
Module/iproxy/idevicedate.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/idevicedebug.exe
Normal file
BIN
Module/iproxy/idevicedebug.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/idevicedebugserverproxy.exe
Normal file
BIN
Module/iproxy/idevicedebugserverproxy.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/idevicedevmodectl.exe
Normal file
BIN
Module/iproxy/idevicedevmodectl.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/idevicediagnostics.exe
Normal file
BIN
Module/iproxy/idevicediagnostics.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/ideviceenterrecovery.exe
Normal file
BIN
Module/iproxy/ideviceenterrecovery.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/ideviceimagemounter.exe
Normal file
BIN
Module/iproxy/ideviceimagemounter.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/ideviceinfo.exe
Normal file
BIN
Module/iproxy/ideviceinfo.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/idevicename.exe
Normal file
BIN
Module/iproxy/idevicename.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/idevicenotificationproxy.exe
Normal file
BIN
Module/iproxy/idevicenotificationproxy.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/idevicepair.exe
Normal file
BIN
Module/iproxy/idevicepair.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/ideviceprovision.exe
Normal file
BIN
Module/iproxy/ideviceprovision.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/idevicerestore.exe
Normal file
BIN
Module/iproxy/idevicerestore.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/idevicescreenshot.exe
Normal file
BIN
Module/iproxy/idevicescreenshot.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/idevicesetlocation.exe
Normal file
BIN
Module/iproxy/idevicesetlocation.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/idevicesyslog.exe
Normal file
BIN
Module/iproxy/idevicesyslog.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/inetcat.exe
Normal file
BIN
Module/iproxy/inetcat.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/irecovery.exe
Normal file
BIN
Module/iproxy/irecovery.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/libcrypto-3.dll
Normal file
BIN
Module/iproxy/libcrypto-3.dll
Normal file
Binary file not shown.
BIN
Module/iproxy/libcurl.dll
Normal file
BIN
Module/iproxy/libcurl.dll
Normal file
Binary file not shown.
BIN
Module/iproxy/libssl-3.dll
Normal file
BIN
Module/iproxy/libssl-3.dll
Normal file
Binary file not shown.
BIN
Module/iproxy/plistutil.exe
Normal file
BIN
Module/iproxy/plistutil.exe
Normal file
Binary file not shown.
BIN
Module/iproxy/readline.dll
Normal file
BIN
Module/iproxy/readline.dll
Normal file
Binary file not shown.
BIN
Module/iproxy/zip.dll
Normal file
BIN
Module/iproxy/zip.dll
Normal file
Binary file not shown.
BIN
Module/iproxy/zlib1.dll
Normal file
BIN
Module/iproxy/zlib1.dll
Normal file
Binary file not shown.
@@ -1,78 +1,76 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
|
||||
class LogManager:
|
||||
# 获取项目根目录
|
||||
projectRoot = os.path.dirname(os.path.dirname(__file__))
|
||||
logDir = os.path.join(projectRoot, "log")
|
||||
# 运行根目录:打包后取 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 = {}
|
||||
|
||||
@classmethod
|
||||
def _setupLogger(cls, udid, name, logName, level=logging.INFO):
|
||||
"""设置日志记录器"""
|
||||
"""创建或获取 logger,并绑定到文件"""
|
||||
deviceLogDir = os.path.join(cls.logDir, udid)
|
||||
os.makedirs(deviceLogDir, exist_ok=True) # 确保日志目录存在
|
||||
os.makedirs(deviceLogDir, exist_ok=True)
|
||||
logFile = os.path.join(deviceLogDir, logName)
|
||||
logger = logging.getLogger(f"{udid}_{name}")
|
||||
|
||||
logger_name = f"{udid}_{name}"
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.setLevel(level)
|
||||
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)
|
||||
|
||||
# 避免重复添加 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 _getLogger(cls, udid, name, logName, level=logging.INFO):
|
||||
"""获取或初始化日志记录器"""
|
||||
if udid not in cls._loggers:
|
||||
cls._loggers[udid] = {}
|
||||
if name not in cls._loggers[udid]:
|
||||
cls._loggers[udid][name] = cls._setupLogger(udid, name, logName, level)
|
||||
return cls._loggers[udid][name]
|
||||
|
||||
@classmethod
|
||||
def info(cls, text, udid):
|
||||
"""记录 INFO 级别的日志"""
|
||||
logger = cls._getLogger(udid, "infoLogger", "info.log", level=logging.INFO)
|
||||
logger.info(f"[{udid}] {text}")
|
||||
cls._setupLogger(udid, "infoLogger", "info.log", level=logging.INFO).info(f"[{udid}] {text}")
|
||||
|
||||
@classmethod
|
||||
def warning(cls, text, udid):
|
||||
"""记录 WARNING 级别的日志"""
|
||||
logger = cls._getLogger(udid, "warningLogger", "warning.log", level=logging.WARNING)
|
||||
logger.warning(f"[{udid}] {text}")
|
||||
cls._setupLogger(udid, "warningLogger", "warning.log", level=logging.WARNING).warning(f"[{udid}] {text}")
|
||||
|
||||
@classmethod
|
||||
def error(cls, text, udid):
|
||||
"""记录 ERROR 级别的日志"""
|
||||
logger = cls._getLogger(udid, "errorLogger", "error.log", level=logging.ERROR)
|
||||
logger.error(f"[{udid}] {text}")
|
||||
cls._setupLogger(udid, "errorLogger", "error.log", level=logging.ERROR).error(f"[{udid}] {text}")
|
||||
|
||||
@classmethod
|
||||
def clearLogs(cls):
|
||||
"""清空整个 log 目录下的所有内容"""
|
||||
"""启动时清空 log 目录"""
|
||||
print("开始清空日志...")
|
||||
|
||||
# 关闭所有日志记录器的处理器
|
||||
for udid in cls._loggers:
|
||||
for name in cls._loggers[udid]:
|
||||
logger = cls._loggers[udid][name]
|
||||
for handler in logger.handlers[:]: # 使用切片避免在迭代时修改列表
|
||||
handler.close()
|
||||
# 关闭所有 handler
|
||||
for name, logger in logging.Logger.manager.loggerDict.items():
|
||||
if isinstance(logger, logging.Logger):
|
||||
for handler in logger.handlers[:]:
|
||||
try:
|
||||
handler.close()
|
||||
except:
|
||||
pass
|
||||
logger.removeHandler(handler)
|
||||
print(f"关闭了 {udid}_{name} 的处理器")
|
||||
|
||||
# 删除整个 log 目录
|
||||
# 删除并重建日志目录
|
||||
if os.path.exists(cls.logDir):
|
||||
shutil.rmtree(cls.logDir) # 删除目录及其所有内容
|
||||
shutil.rmtree(cls.logDir)
|
||||
print(f"删除了 {cls.logDir}")
|
||||
os.makedirs(cls.logDir, exist_ok=True) # 重新创建空的 log 目录
|
||||
print(f"重新创建了 {cls.logDir}")
|
||||
else:
|
||||
print(f"{cls.logDir} 不存在,无需删除")
|
||||
os.makedirs(cls.logDir, exist_ok=True)
|
||||
print(f"重新创建了 {cls.logDir}")
|
||||
print("日志清空完成")
|
||||
23
build.bat
Normal file
23
build.bat
Normal file
@@ -0,0 +1,23 @@
|
||||
python -m nuitka Module/Main.py ^
|
||||
--standalone ^
|
||||
--msvc=latest ^
|
||||
--windows-console-mode=force ^
|
||||
--remove-output ^
|
||||
--output-dir=out ^
|
||||
--output-filename=IOSAI ^
|
||||
--include-package=Module,Utils,Entity,script ^
|
||||
--include-module=flask ^
|
||||
--include-module=flask_cors ^
|
||||
--include-module=jinja2 ^
|
||||
--include-module=werkzeug ^
|
||||
--include-module=cv2 ^
|
||||
--include-module=numpy ^
|
||||
--include-module=lxml ^
|
||||
--include-module=lxml.etree ^
|
||||
--include-module=requests ^
|
||||
--include-module=urllib3 ^
|
||||
--include-module=certifi ^
|
||||
--include-module=idna ^
|
||||
--include-data-dir=resources=resources ^
|
||||
--include-data-dir=Module/iproxy=Module/iproxy ^
|
||||
--windows-icon-from-ico=resources/icon.ico
|
||||
BIN
resources/icon.ico
Normal file
BIN
resources/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@@ -451,6 +451,7 @@ class ScriptManager():
|
||||
print("监控回复消息")
|
||||
# 执行回复消息逻辑
|
||||
self.monitorMessages(session, udid)
|
||||
# 判断是否有首页按钮
|
||||
homeButton = AiUtils.findHomeButton(udid)
|
||||
if homeButton.exists:
|
||||
homeButton.click()
|
||||
@@ -469,12 +470,12 @@ class ScriptManager():
|
||||
session.appium_settings({"snapshotMaxDepth": 15})
|
||||
# 点击搜索按钮
|
||||
ControlUtils.clickSearch(session)
|
||||
|
||||
else:
|
||||
session.appium_settings({"snapshotMaxDepth": 15})
|
||||
# 点击搜索按钮
|
||||
ControlUtils.clickSearch(session)
|
||||
|
||||
|
||||
def replyMessages(self, udid, event):
|
||||
client = wda.USBClient(udid)
|
||||
session = client.session()
|
||||
|
||||
Reference in New Issue
Block a user