临时提交
@@ -1,111 +1,199 @@
|
|||||||
import subprocess
|
# -*- coding: utf-8 -*-
|
||||||
import threading
|
import os
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import json
|
||||||
import wda
|
import wda
|
||||||
|
import threading
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Dict, Optional
|
||||||
|
|
||||||
from tidevice import Usbmux
|
from tidevice import Usbmux
|
||||||
from Entity.DeviceModel import DeviceModel
|
from Entity.DeviceModel import DeviceModel
|
||||||
from Entity.Variables import WdaAppBundleId
|
from Entity.Variables import WdaAppBundleId
|
||||||
from Module.FlaskSubprocessManager import FlaskSubprocessManager
|
from Module.FlaskSubprocessManager import FlaskSubprocessManager
|
||||||
from Utils.LogManager import LogManager
|
from Utils.LogManager import LogManager
|
||||||
|
|
||||||
threadLock = threading.Lock()
|
|
||||||
|
|
||||||
class Deviceinfo(object):
|
class Deviceinfo(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.deviceIndex = 0
|
self.deviceIndex = 0
|
||||||
# 投屏端口
|
# 投屏端口(本地映射端口起始值,会递增)
|
||||||
self.screenProxy = 9110
|
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):
|
def startDeviceListener(self):
|
||||||
|
LogManager.info("Device Listener started", "listener")
|
||||||
while True:
|
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:
|
for device in lists:
|
||||||
if device not in self.deviceArray:
|
if device not in self.deviceArray:
|
||||||
self.screenProxy += 1
|
self.screenProxy += 1
|
||||||
self.connectDevice(device.udid)
|
try:
|
||||||
self.deviceArray.append(device)
|
self.connectDevice(device.udid)
|
||||||
|
self.deviceArray.append(device)
|
||||||
|
except Exception as e:
|
||||||
|
LogManager.error(f"连接设备失败 {device.udid}: {e}", device.udid)
|
||||||
|
|
||||||
# 处理拔出设备的逻辑
|
# 拔出设备处理
|
||||||
def removeDevice():
|
self._removeDisconnected(lists)
|
||||||
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)
|
|
||||||
|
|
||||||
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)
|
time.sleep(1)
|
||||||
|
|
||||||
# 连接设备
|
# ----------------------------
|
||||||
def connectDevice(self, identifier):
|
# 连接单台设备:启动 WDA、读取屏参、通知前端、映射投屏端口
|
||||||
|
# ----------------------------
|
||||||
|
def connectDevice(self, identifier: str):
|
||||||
|
# 1) 连接 WDA(USBClient -> 设备 8100)
|
||||||
try:
|
try:
|
||||||
d = wda.USBClient(identifier, 8100)
|
d = wda.USBClient(identifier, 8100)
|
||||||
LogManager.info("启动wda成功", identifier)
|
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())
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LogManager.error("启动wda失败。请检查wda是否正常", identifier)
|
LogManager.error(f"启动 WDA 失败,请检查手机是否已信任、WDA 是否正常。错误: {e}", identifier)
|
||||||
return
|
return # 不抛出到外层,保持监听循环健壮
|
||||||
|
|
||||||
d.app_start(WdaAppBundleId)
|
# 2) 读取屏幕信息(失败不影响主流程)
|
||||||
d.home()
|
width, height, scale = 0, 0, 1.0
|
||||||
time.sleep(2)
|
|
||||||
target = self.relayDeviceScreenPort(identifier)
|
|
||||||
self.pidList.append({
|
|
||||||
"target": target,
|
|
||||||
"id": identifier
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 转发设备端口
|
|
||||||
def relayDeviceScreenPort(self, udid):
|
|
||||||
try:
|
try:
|
||||||
command = f"iproxy.exe -u {udid} {self.screenProxy} 9100"
|
size = d.window_size()
|
||||||
# 创建一个没有窗口的进程
|
width, height = size.width, size.height
|
||||||
startupinfo = subprocess.STARTUPINFO()
|
scale = d.scale
|
||||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
||||||
startupinfo.wShowWindow = 0
|
|
||||||
r = subprocess.Popen(command, shell=True, startupinfo=startupinfo)
|
|
||||||
return r
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
LogManager.warning(f"读取屏幕信息失败:{e}", identifier)
|
||||||
return 0
|
|
||||||
|
# 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)
|
client = wda.USBClient(udid)
|
||||||
session = client.session()
|
session = client.session()
|
||||||
session.appium_settings({"snapshotMaxDepth": 0})
|
session.appium_settings({"snapshotMaxDepth": 0})
|
||||||
|
|
||||||
x = body.get("x")
|
x = body.get("x")
|
||||||
y = body.get("y")
|
y = body.get("y")
|
||||||
|
|
||||||
session.tap(x, y)
|
session.tap(x, y)
|
||||||
return ResultData(data="").toJson()
|
return ResultData(data="").toJson()
|
||||||
|
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import atexit
|
import atexit
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
|
from pathlib import Path
|
||||||
from typing import Optional, Union, Dict, List
|
from typing import Optional, Union, Dict, List
|
||||||
|
|
||||||
class FlaskSubprocessManager:
|
class FlaskSubprocessManager:
|
||||||
@@ -20,38 +22,59 @@ class FlaskSubprocessManager:
|
|||||||
|
|
||||||
def _init_manager(self):
|
def _init_manager(self):
|
||||||
self.process: Optional[subprocess.Popen] = None
|
self.process: Optional[subprocess.Popen] = None
|
||||||
self.comm_port = self._find_available_port()
|
self.comm_port = 34567
|
||||||
self._stop_event = threading.Event()
|
self._stop_event = threading.Event()
|
||||||
atexit.register(self.stop)
|
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:
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
s.bind(('0.0.0.0', 0))
|
s.settimeout(0.2)
|
||||||
return s.getsockname()[1]
|
return s.connect_ex(("127.0.0.1", port)) == 0
|
||||||
|
|
||||||
|
# 启动flask
|
||||||
def start(self):
|
def start(self):
|
||||||
"""启动子进程(Windows兼容方案)"""
|
"""启动 Flask 子进程(兼容打包后的 exe 和源码运行)"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if self.process is not None:
|
if self.process is not None:
|
||||||
raise RuntimeError("子进程已在运行中!")
|
raise RuntimeError("子进程已在运行中!")
|
||||||
# 通过环境变量传递通信端口
|
|
||||||
env = os.environ.copy()
|
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(
|
self.process = subprocess.Popen(
|
||||||
['python', 'Flask/FlaskService.py'], # 启动一个子进程 FlaskService.py
|
cmd,
|
||||||
stdin=subprocess.PIPE, # 标准输入流,用于向子进程发送数据
|
stdin=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE, # 标准输出流,用于接收子进程的输出
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE, # 标准错误流,用于接收子进程的错误信息
|
stderr=subprocess.PIPE,
|
||||||
text=True, # 以文本模式打开流,否则以二进制模式打开
|
text=True,
|
||||||
bufsize=1, # 缓冲区大小设置为 1,表示行缓冲
|
encoding="utf-8",
|
||||||
encoding='utf-8', # 指定编码为 UTF-8,确保控制台输出不会报错
|
errors="replace", # 新增:遇到非 UTF-8 字节用 <20> 代替,避免崩溃
|
||||||
env=env # 指定子进程的环境变量
|
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):
|
def print_output(stream, stream_name):
|
||||||
while True:
|
while True:
|
||||||
line = stream.readline()
|
line = stream.readline()
|
||||||
@@ -59,7 +82,6 @@ class FlaskSubprocessManager:
|
|||||||
break
|
break
|
||||||
print(f"{stream_name}: {line.strip()}")
|
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.stdout, "STDOUT"), daemon=True).start()
|
||||||
threading.Thread(target=print_output, args=(self.process.stderr, "STDERR"), 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
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from Module.DeviceInfo import Deviceinfo
|
from Module.DeviceInfo import Deviceinfo
|
||||||
from Module.FlaskSubprocessManager import FlaskSubprocessManager
|
from Module.FlaskSubprocessManager import FlaskSubprocessManager
|
||||||
from Utils.LogManager import LogManager
|
from Utils.LogManager import LogManager
|
||||||
|
|
||||||
# 项目入口
|
# 确定 exe 或 py 文件所在目录
|
||||||
if __name__ == "__main__":
|
BASE = Path(getattr(sys, 'frozen', False) and sys.executable or __file__).resolve().parent
|
||||||
# 清空日志
|
LOG_DIR = BASE / "log"
|
||||||
LogManager.clearLogs()
|
LOG_DIR.mkdir(exist_ok=True) # 确保 log 目录存在
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
|
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 = FlaskSubprocessManager.get_instance()
|
||||||
manager.start()
|
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/getopt.dll
Normal file
BIN
Module/iproxy/idevice_id.exe
Normal file
BIN
Module/iproxy/idevicebackup.exe
Normal file
BIN
Module/iproxy/idevicebackup2.exe
Normal file
BIN
Module/iproxy/idevicebtlogger.exe
Normal file
BIN
Module/iproxy/idevicecrashreport.exe
Normal file
BIN
Module/iproxy/idevicedate.exe
Normal file
BIN
Module/iproxy/idevicedebug.exe
Normal file
BIN
Module/iproxy/idevicedebugserverproxy.exe
Normal file
BIN
Module/iproxy/idevicedevmodectl.exe
Normal file
BIN
Module/iproxy/idevicediagnostics.exe
Normal file
BIN
Module/iproxy/ideviceenterrecovery.exe
Normal file
BIN
Module/iproxy/ideviceimagemounter.exe
Normal file
BIN
Module/iproxy/ideviceinfo.exe
Normal file
BIN
Module/iproxy/idevicename.exe
Normal file
BIN
Module/iproxy/idevicenotificationproxy.exe
Normal file
BIN
Module/iproxy/idevicepair.exe
Normal file
BIN
Module/iproxy/ideviceprovision.exe
Normal file
BIN
Module/iproxy/idevicerestore.exe
Normal file
BIN
Module/iproxy/idevicescreenshot.exe
Normal file
BIN
Module/iproxy/idevicesetlocation.exe
Normal file
BIN
Module/iproxy/idevicesyslog.exe
Normal file
BIN
Module/iproxy/inetcat.exe
Normal file
BIN
Module/iproxy/irecovery.exe
Normal file
BIN
Module/iproxy/libcrypto-3.dll
Normal file
BIN
Module/iproxy/libcurl.dll
Normal file
BIN
Module/iproxy/libssl-3.dll
Normal file
BIN
Module/iproxy/plistutil.exe
Normal file
BIN
Module/iproxy/readline.dll
Normal file
BIN
Module/iproxy/zip.dll
Normal file
BIN
Module/iproxy/zlib1.dll
Normal file
@@ -1,78 +1,76 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
class LogManager:
|
class LogManager:
|
||||||
# 获取项目根目录
|
# 运行根目录:打包后取 exe 目录;源码运行取项目目录
|
||||||
projectRoot = os.path.dirname(os.path.dirname(__file__))
|
if getattr(sys, "frozen", False):
|
||||||
logDir = os.path.join(projectRoot, "log")
|
projectRoot = os.path.dirname(sys.executable)
|
||||||
|
else:
|
||||||
|
projectRoot = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
|
||||||
# 类变量,存储日志记录器
|
logDir = os.path.join(projectRoot, "log")
|
||||||
_loggers = {}
|
_loggers = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _setupLogger(cls, udid, name, logName, level=logging.INFO):
|
def _setupLogger(cls, udid, name, logName, level=logging.INFO):
|
||||||
"""设置日志记录器"""
|
"""创建或获取 logger,并绑定到文件"""
|
||||||
deviceLogDir = os.path.join(cls.logDir, udid)
|
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)
|
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)
|
logger.setLevel(level)
|
||||||
fileHandler = logging.FileHandler(logFile, mode="a", encoding="utf-8")
|
|
||||||
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
# 避免重复添加 handler
|
||||||
datefmt="%Y-%m-%d %H:%M:%S")
|
if not any(
|
||||||
fileHandler.setFormatter(formatter)
|
isinstance(h, logging.FileHandler) and h.baseFilename == os.path.abspath(logFile)
|
||||||
logger.addHandler(fileHandler)
|
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
|
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
|
@classmethod
|
||||||
def info(cls, text, udid):
|
def info(cls, text, udid):
|
||||||
"""记录 INFO 级别的日志"""
|
cls._setupLogger(udid, "infoLogger", "info.log", level=logging.INFO).info(f"[{udid}] {text}")
|
||||||
logger = cls._getLogger(udid, "infoLogger", "info.log", level=logging.INFO)
|
|
||||||
logger.info(f"[{udid}] {text}")
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def warning(cls, text, udid):
|
def warning(cls, text, udid):
|
||||||
"""记录 WARNING 级别的日志"""
|
cls._setupLogger(udid, "warningLogger", "warning.log", level=logging.WARNING).warning(f"[{udid}] {text}")
|
||||||
logger = cls._getLogger(udid, "warningLogger", "warning.log", level=logging.WARNING)
|
|
||||||
logger.warning(f"[{udid}] {text}")
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def error(cls, text, udid):
|
def error(cls, text, udid):
|
||||||
"""记录 ERROR 级别的日志"""
|
cls._setupLogger(udid, "errorLogger", "error.log", level=logging.ERROR).error(f"[{udid}] {text}")
|
||||||
logger = cls._getLogger(udid, "errorLogger", "error.log", level=logging.ERROR)
|
|
||||||
logger.error(f"[{udid}] {text}")
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clearLogs(cls):
|
def clearLogs(cls):
|
||||||
"""清空整个 log 目录下的所有内容"""
|
"""启动时清空 log 目录"""
|
||||||
print("开始清空日志...")
|
print("开始清空日志...")
|
||||||
|
|
||||||
# 关闭所有日志记录器的处理器
|
# 关闭所有 handler
|
||||||
for udid in cls._loggers:
|
for name, logger in logging.Logger.manager.loggerDict.items():
|
||||||
for name in cls._loggers[udid]:
|
if isinstance(logger, logging.Logger):
|
||||||
logger = cls._loggers[udid][name]
|
for handler in logger.handlers[:]:
|
||||||
for handler in logger.handlers[:]: # 使用切片避免在迭代时修改列表
|
try:
|
||||||
handler.close()
|
handler.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
logger.removeHandler(handler)
|
logger.removeHandler(handler)
|
||||||
print(f"关闭了 {udid}_{name} 的处理器")
|
|
||||||
|
|
||||||
# 删除整个 log 目录
|
# 删除并重建日志目录
|
||||||
if os.path.exists(cls.logDir):
|
if os.path.exists(cls.logDir):
|
||||||
shutil.rmtree(cls.logDir) # 删除目录及其所有内容
|
shutil.rmtree(cls.logDir)
|
||||||
print(f"删除了 {cls.logDir}")
|
print(f"删除了 {cls.logDir}")
|
||||||
os.makedirs(cls.logDir, exist_ok=True) # 重新创建空的 log 目录
|
os.makedirs(cls.logDir, exist_ok=True)
|
||||||
print(f"重新创建了 {cls.logDir}")
|
print(f"重新创建了 {cls.logDir}")
|
||||||
else:
|
print("日志清空完成")
|
||||||
print(f"{cls.logDir} 不存在,无需删除")
|
|
||||||
print("日志清空完成")
|
|
||||||
|
|||||||
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
out/Main.dist/IOSAI.exe
Normal file
4738
out/Main.dist/certifi/cacert.pem
Normal file
BIN
out/Main.dist/cv2/opencv_videoio_ffmpeg4120_64.dll
Normal file
2
out/Main.dist/jaraco/text/Lorem ipsum.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||||
|
Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst.
|
||||||
BIN
out/Main.dist/libcrypto-3.dll
Normal file
BIN
out/Main.dist/libffi-8.dll
Normal file
BIN
out/Main.dist/libssl-3.dll
Normal file
BIN
out/Main.dist/python3.dll
Normal file
BIN
out/Main.dist/python312.dll
Normal file
BIN
out/Main.dist/pythoncom312.dll
Normal file
BIN
out/Main.dist/pywintypes312.dll
Normal file
BIN
out/Main.dist/resources/add.png
Normal file
|
After Width: | Height: | Size: 899 B |
BIN
out/Main.dist/resources/advertisement.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
out/Main.dist/resources/back.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
out/Main.dist/resources/comment.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
out/Main.dist/resources/icon.ico
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
out/Main.dist/resources/like.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
out/Main.dist/resources/search.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
out/Main.dist/tcl86t.dll
Normal file
BIN
out/Main.dist/tk86t.dll
Normal file
BIN
out/Main.dist/vcruntime140.dll
Normal file
BIN
out/Main.dist/vcruntime140_1.dll
Normal file
BIN
out/Main.dist/zlib1.dll
Normal file
BIN
resources/icon.ico
Normal file
|
After Width: | Height: | Size: 51 KiB |
@@ -348,6 +348,7 @@ class ScriptManager():
|
|||||||
if AiUtils.getUnReadMsgCount(session) > 0:
|
if AiUtils.getUnReadMsgCount(session) > 0:
|
||||||
# 执行回复消息逻辑
|
# 执行回复消息逻辑
|
||||||
self.monitorMessages(session, udid)
|
self.monitorMessages(session, udid)
|
||||||
|
# 判断是否有首页按钮
|
||||||
homeButton = AiUtils.findHomeButton(udid)
|
homeButton = AiUtils.findHomeButton(udid)
|
||||||
if homeButton.exists:
|
if homeButton.exists:
|
||||||
homeButton.click()
|
homeButton.click()
|
||||||
@@ -362,12 +363,12 @@ class ScriptManager():
|
|||||||
session.appium_settings({"snapshotMaxDepth": 15})
|
session.appium_settings({"snapshotMaxDepth": 15})
|
||||||
# 点击搜索按钮
|
# 点击搜索按钮
|
||||||
ControlUtils.clickSearch(session)
|
ControlUtils.clickSearch(session)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
session.appium_settings({"snapshotMaxDepth": 15})
|
session.appium_settings({"snapshotMaxDepth": 15})
|
||||||
# 点击搜索按钮
|
# 点击搜索按钮
|
||||||
ControlUtils.clickSearch(session)
|
ControlUtils.clickSearch(session)
|
||||||
|
|
||||||
|
|
||||||
def replyMessages(self, udid, event):
|
def replyMessages(self, udid, event):
|
||||||
client = wda.USBClient(udid)
|
client = wda.USBClient(udid)
|
||||||
session = client.session()
|
session = client.session()
|
||||||
|
|||||||