590 lines
24 KiB
Python
590 lines
24 KiB
Python
# coding: utf-8
|
||
"""
|
||
App Store 应用安装器
|
||
|
||
基于 IOSAutomationBase 实现的 App Store 自动安装功能
|
||
"""
|
||
|
||
from typing import Optional, Tuple, Dict
|
||
import time
|
||
import subprocess
|
||
import csv
|
||
import os
|
||
import re
|
||
from datetime import datetime, timezone, timedelta
|
||
|
||
# 使用绝对导入
|
||
try:
|
||
from utils_ios.package_init.ios_automation_base import IOSAutomationBase, logger
|
||
except ImportError:
|
||
from ios_automation_base import IOSAutomationBase, logger
|
||
|
||
|
||
class AppStoreInstaller(IOSAutomationBase):
|
||
"""App Store 应用安装器"""
|
||
|
||
# CSV文件路径
|
||
CSV_FILE = "output/ios/appstore_mapping.csv"
|
||
|
||
def __init__(self, server_url: str, udid: Optional[str] = None,
|
||
ssh_host: Optional[str] = None, go_ios: Optional[str] = 'ios',
|
||
alert_strategy: str = "allow", health_monitor=None):
|
||
"""
|
||
初始化安装器
|
||
|
||
Args:
|
||
server_url: WDA Server URL
|
||
udid: 设备UDID(用于go-ios)
|
||
ssh_host: SSH主机地址(用于远程执行go-ios)
|
||
go_ios: go-ios可执行文件位置
|
||
alert_strategy: Alert 处理策略 - "allow" 或 "deny"
|
||
health_monitor: WDAHealthMonitor 实例(可选)
|
||
"""
|
||
super().__init__(server_url, default_alert_strategy=alert_strategy,
|
||
udid=udid, ssh_host=ssh_host, go_ios=go_ios, health_monitor=health_monitor)
|
||
self.udid = udid
|
||
self.ssh_host = ssh_host
|
||
self.go_ios = go_ios
|
||
self._health_monitor = health_monitor # 保存引用,供等待循环直接使用
|
||
|
||
def _format_app_id(self, app_id: str) -> str:
|
||
"""格式化app_id"""
|
||
if re.match(r'^\d+$', app_id):
|
||
formatted = f"id{app_id}"
|
||
logger.debug(f"格式化 app_id: {app_id} -> {formatted}")
|
||
return formatted
|
||
elif re.match(r'^id\d+$', app_id):
|
||
logger.debug(f"app_id 已是标准格式: {app_id}")
|
||
return app_id
|
||
else:
|
||
logger.debug(f"app_id 使用 slug 格式: {app_id}")
|
||
return app_id
|
||
|
||
def _open_appstore_page(self, app_id: str) -> bool:
|
||
"""打开App Store详情页"""
|
||
target_url = f"itms-apps://itunes.apple.com/app/{app_id}"
|
||
return self._open_url(target_url)
|
||
|
||
def _check_network(self) -> bool:
|
||
"""检查网络连接(通过 Alert API 检测网络错误弹窗)"""
|
||
network_texts = ["无互联网连接"]
|
||
try:
|
||
alert = self.client.alert
|
||
if alert.exists:
|
||
alert_text = alert.text or ""
|
||
for keyword in network_texts:
|
||
if keyword in alert_text:
|
||
logger.error(f"网络连接异常 (Alert 文本): {alert_text}")
|
||
return False
|
||
except Exception as e:
|
||
logger.debug(f"检查网络 Alert 异常: {e}")
|
||
return True
|
||
|
||
def _check_app_unavailable(self) -> bool:
|
||
"""检查App是否不可用(通过 WDA Alert API 直接读取弹窗文本)
|
||
|
||
系统弹窗不属于页面元素树,必须通过 alert API 获取文本,
|
||
不能通过 finder.check_exists 搜索页面元素。
|
||
"""
|
||
unavailable_texts = [
|
||
"不可用", "App不可用",
|
||
"無法取得", "无法获取"
|
||
]
|
||
|
||
try:
|
||
alert = self.client.alert
|
||
if alert.exists:
|
||
alert_text = alert.text or ""
|
||
for keyword in unavailable_texts:
|
||
if keyword in alert_text:
|
||
logger.warning(f"应用不可用 (Alert 文本): {alert_text}")
|
||
return True
|
||
# Alert 存在但不是不可用弹窗,记录日志供调试
|
||
logger.debug(f"检测到 Alert 但非不可用弹窗: {alert_text}")
|
||
except Exception as e:
|
||
logger.debug(f"检查应用不可用 Alert 异常: {e}")
|
||
return False
|
||
|
||
def _check_already_installed(self) -> bool:
|
||
"""检查是否已安装"""
|
||
open_buttons = ["打开", "Open", "open"]
|
||
found, label = self.finder.check_exists(open_buttons, check_types=['label'])
|
||
|
||
if found:
|
||
logger.info(f"应用已安装 (发现 '{label}' 按钮)")
|
||
return True
|
||
return False
|
||
|
||
def _find_and_click_get_button(self, max_retries: int = 2) -> bool:
|
||
"""查找并点击获取按钮,失败时重试"""
|
||
get_buttons = ["获取", "重新下载", "更新", "继续"]
|
||
|
||
for attempt in range(max_retries):
|
||
clicked, label = self.finder.find_and_click(get_buttons, timeout=2.0)
|
||
if clicked:
|
||
logger.info(f"点击获取按钮成功: {label}")
|
||
time.sleep(1)
|
||
return True
|
||
|
||
if attempt < max_retries - 1:
|
||
logger.warning(f"点击获取按钮失败 (第{attempt + 1}次),等待 WDA 恢复后重试...")
|
||
try:
|
||
self.client.wait_ready(3)
|
||
except Exception as e:
|
||
logger.warning(f"WDA wait_ready 异常: {e}")
|
||
time.sleep(1)
|
||
|
||
logger.error("点击获取按钮最终失败")
|
||
return False
|
||
|
||
def _check_paid_app(self) -> Tuple[bool, Optional[str]]:
|
||
"""
|
||
检查是否为付费应用。
|
||
|
||
通过 name="AppStore.offerButton[state=get]" 定位应用详情页的下载/价格按钮,
|
||
避免因页面其他金额文本(如订阅价格)导致误判。
|
||
调试发现:
|
||
- 免费应用:按钮 label == '重新下载' 或 '获取'
|
||
- 付费应用:按钮 label == 'US$8.99' 等价格字符串
|
||
"""
|
||
# 价格格式正则:货币代码+符号+金额,如 US$8.99、¥12、€4.99
|
||
price_pattern = re.compile(
|
||
r'^[A-Z]{0,3}[$¥€£₹₩₽฿₫₺]\d+(?:[.,]\d+)?$'
|
||
r'|^[$¥€£₹₩₽฿₫₺]\d+(?:[.,]\d+)?$',
|
||
re.IGNORECASE
|
||
)
|
||
# 确认为免费下载的按钮 label(排除误判)
|
||
free_labels = {"获取", "重新下载", "更新", "打开", "Open", "Get"}
|
||
|
||
try:
|
||
buttons = self.client(name="AppStore.offerButton[state=get]").find_elements()
|
||
if not buttons:
|
||
logger.debug("未找到 offerButton,跳过付费检查")
|
||
return False, None
|
||
|
||
btn = buttons[0]
|
||
# find_elements() 返回 WDA Element 对象,通过 .label 属性访问
|
||
label = getattr(btn, 'label', None) or getattr(btn, 'text', None) or ""
|
||
logger.debug(f"付费检查按钮: label='{label}'")
|
||
|
||
if label in free_labels:
|
||
# 明确是免费下载按钮
|
||
return False, None
|
||
|
||
if price_pattern.match(label.strip()):
|
||
logger.warning(f"这是付费应用: {label}")
|
||
return True, label
|
||
|
||
except Exception as e:
|
||
logger.debug(f"检查付费应用异常: {e}")
|
||
|
||
return False, None
|
||
|
||
def _wait_loading_finish(self, loading_timeout: float = 30.0) -> bool:
|
||
"""等待"正在载入"状态结束
|
||
|
||
点击获取按钮后,App Store 可能显示"正在载入"控件,
|
||
网络较慢时可能持续十几秒,需要等待其消失后安装按钮才会出现。
|
||
|
||
Returns:
|
||
True: 载入状态已结束(或从未出现)
|
||
False: 超时仍在载入
|
||
"""
|
||
loading_labels = ["正在载入", "正在載入", "Loading"]
|
||
|
||
# 先检查是否存在"正在载入"控件
|
||
found, label = self.finder.check_exists(loading_labels, check_types=['label'])
|
||
if not found:
|
||
logger.debug("未检测到'正在载入'状态,直接继续")
|
||
return True
|
||
|
||
logger.info(f"检测到'{label}'状态,等待加载完成(超时: {loading_timeout}秒)...")
|
||
start_time = time.time()
|
||
|
||
while time.time() - start_time < loading_timeout:
|
||
time.sleep(2)
|
||
try:
|
||
found, label = self.finder.check_exists(loading_labels, check_types=['label'])
|
||
except Exception as e:
|
||
logger.warning(f"检查载入状态异常: {e}")
|
||
continue
|
||
|
||
if not found:
|
||
elapsed = time.time() - start_time
|
||
logger.info(f"'正在载入'状态已结束,耗时 {elapsed:.1f}秒")
|
||
return True
|
||
|
||
logger.warning(f"等待'正在载入'超时 ({loading_timeout}秒)")
|
||
return False
|
||
|
||
def _confirm_install(self, max_retries: int = 3) -> bool:
|
||
"""确认安装,失败时重试
|
||
|
||
流程:
|
||
1. 等待"正在载入"状态结束(网络慢时可能需要较长时间)
|
||
2. 查找并点击"安装"确认按钮
|
||
|
||
WDA 可能在此步骤超时(如 wait_ready 超时导致点击未执行),
|
||
因此需要重试机制保证安装确认可靠执行。
|
||
"""
|
||
# 先等待"正在载入"状态结束
|
||
self._wait_loading_finish(loading_timeout=30.0)
|
||
|
||
install_buttons = ["安装", "Install", "install"]
|
||
|
||
for attempt in range(max_retries):
|
||
clicked, label = self.finder.find_and_click(install_buttons, timeout=3.0)
|
||
if clicked:
|
||
logger.info(f"点击安装确认按钮成功: {label}")
|
||
return True
|
||
|
||
if attempt < max_retries - 1:
|
||
logger.warning(f"点击安装确认按钮失败 (第{attempt + 1}次),等待 WDA 恢复后重试...")
|
||
try:
|
||
self.client.wait_ready(3)
|
||
except Exception as e:
|
||
logger.warning(f"WDA wait_ready 异常: {e}")
|
||
time.sleep(2)
|
||
|
||
# 安装确认按钮可能本身就不存在(不需要二次确认的情况),这不算错误
|
||
logger.debug("未找到安装确认按钮(可能无需二次确认)")
|
||
return False
|
||
|
||
def _handle_password_input(self) -> bool:
|
||
"""处理密码输入"""
|
||
# 等待页面稳定
|
||
self.client.wait_ready(2)
|
||
|
||
# 检查是否需要登录
|
||
signin_labels = ["登录", "登錄", "Sign In", "sign in"]
|
||
needs_password, found_label = self.finder.check_exists(signin_labels, auto_handle_alert=False)
|
||
|
||
if not needs_password:
|
||
logger.debug("无需密码验证")
|
||
return True
|
||
|
||
# 获取密码
|
||
ios_password = os.environ.get('IOS_PASSWORD', '')
|
||
if not ios_password:
|
||
logger.error("需要密码但未设置 IOS_PASSWORD 环境变量")
|
||
return False
|
||
|
||
# 输入密码
|
||
try:
|
||
logger.info("输入 Apple ID 密码")
|
||
self.client.send_keys(list(ios_password))
|
||
time.sleep(1)
|
||
|
||
# 点击登录按钮
|
||
clicked, label = self.finder.find_and_click(signin_labels, timeout=1.0, auto_handle_alert=False)
|
||
|
||
if not clicked:
|
||
logger.error("未找到登录按钮")
|
||
return False
|
||
|
||
# 等待验证完成
|
||
self.client.wait_ready(3)
|
||
logger.info("密码验证完成")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"密码输入失败: {e}")
|
||
return False
|
||
|
||
def _get_installed_apps_info(self) -> Dict[str, Dict[str, str]]:
|
||
"""调用go-ios获取已安装应用信息"""
|
||
try:
|
||
base_cmd = [self.go_ios, "apps", "--list"]
|
||
if self.udid:
|
||
base_cmd.extend(["--udid", self.udid])
|
||
|
||
if self.ssh_host:
|
||
cmd = ["ssh", self.ssh_host, " ".join(base_cmd)]
|
||
else:
|
||
cmd = base_cmd
|
||
|
||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||
if result.returncode != 0:
|
||
return {}
|
||
|
||
apps_info = {}
|
||
for line in result.stdout.strip().split('\n'):
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
# 输出格式: bundle_id app_name version
|
||
# 版本号是最后一个 token(不含空格),应用名可能含空格
|
||
# 先从右侧切出版本号,再从左侧切出 bundle_id,中间全为应用名
|
||
right_parts = line.rsplit(None, 1) # ['bundle_id app_name', 'version']
|
||
if len(right_parts) == 2:
|
||
left, version = right_parts
|
||
left_parts = left.split(None, 1) # ['bundle_id', 'app_name']
|
||
if len(left_parts) == 2:
|
||
bundle_id, app_name = left_parts
|
||
apps_info[bundle_id] = {"app_name": app_name, "version": version}
|
||
elif len(left_parts) == 1:
|
||
# 行只有两个词:bundle_id 和 version,没有应用名
|
||
apps_info[left_parts[0]] = {"app_name": "", "version": version}
|
||
elif len(right_parts) == 1:
|
||
# 仅 bundle_id,无应用名和版本
|
||
apps_info[right_parts[0]] = {"app_name": "", "version": ""}
|
||
|
||
|
||
return apps_info
|
||
except Exception:
|
||
return {}
|
||
|
||
def _load_app_mapping(self) -> Dict[str, Dict[str, str]]:
|
||
"""从CSV加载映射"""
|
||
mapping = {}
|
||
if not os.path.exists(self.CSV_FILE):
|
||
return mapping
|
||
|
||
try:
|
||
with open(self.CSV_FILE, 'r', encoding='utf-8') as f:
|
||
reader = csv.DictReader(f)
|
||
for row in reader:
|
||
app_id = row.get('app_id', '').strip()
|
||
if app_id:
|
||
mapping[app_id] = {
|
||
"bundle_id": row.get('bundle_id', ''),
|
||
"app_name": row.get('app_name', ''),
|
||
"version": row.get('version', ''),
|
||
"last_updated": row.get('last_updated', '')
|
||
}
|
||
except Exception:
|
||
pass
|
||
|
||
return mapping
|
||
|
||
def _save_app_mapping(self, app_id: str, bundle_id: str,
|
||
app_name: str, version: str):
|
||
"""保存映射到CSV"""
|
||
try:
|
||
# 使用东八区时间(UTC+8)
|
||
tz_cn = timezone(timedelta(hours=8))
|
||
current_time = datetime.now(tz_cn).strftime("%Y-%m-%d %H:%M:%S")
|
||
|
||
mapping = self._load_app_mapping()
|
||
mapping[app_id] = {
|
||
"bundle_id": bundle_id,
|
||
"app_name": app_name,
|
||
"version": version,
|
||
"last_updated": current_time
|
||
}
|
||
|
||
with open(self.CSV_FILE, 'w', encoding='utf-8', newline='') as f:
|
||
fieldnames = ['app_id', 'bundle_id', 'app_name', 'version', 'last_updated']
|
||
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||
writer.writeheader()
|
||
|
||
for aid, info in mapping.items():
|
||
writer.writerow({
|
||
'app_id': aid,
|
||
'bundle_id': info['bundle_id'],
|
||
'app_name': info['app_name'],
|
||
'version': info['version'],
|
||
'last_updated': info['last_updated']
|
||
})
|
||
except Exception:
|
||
pass
|
||
|
||
def _wait_for_installation(self, app_id: str, timeout: float, apps_before: Optional[Dict] = None) -> bool:
|
||
"""
|
||
等待安装完成
|
||
|
||
WDA 断联处理:等待循环中捕获到 WDA 网络异常时,触发自动恢复并继续等待,
|
||
而不是直接退出。恢复失败时才真正抛出异常。
|
||
|
||
Args:
|
||
app_id: App Store 应用 ID
|
||
timeout: 超时时间(秒)
|
||
apps_before: 安装前的应用列表(用于对比检测新安装的应用)
|
||
"""
|
||
logger.info(f"等待安装完成 (超时: {timeout}秒)...")
|
||
|
||
# 等待安装完成
|
||
start_time = time.time()
|
||
open_buttons = ["打开", "Open", "open"]
|
||
|
||
while time.time() - start_time < timeout:
|
||
try:
|
||
# 检查是否出现"打开"按钮
|
||
# find.check_exists 内部已有 _check_health_fast_fail(检测到不健康时等待恢复)
|
||
# 此处额外 try/except 兜底捕获 WDA 网络层直接抛出的连接异常
|
||
found, label = self.finder.check_exists(open_buttons, check_types=['label'])
|
||
except RuntimeError:
|
||
# RuntimeError 来自 _check_health_fast_fail(恢复失败时),直接上抛
|
||
raise
|
||
except Exception as e:
|
||
# WDA 网络层直接抛出的连接异常(连接拒绝、超时等)
|
||
logger.warning(f"安装等待中检测到 WDA 异常: {e}")
|
||
if self._health_monitor:
|
||
logger.info("触发 WDA 恢复并等待...")
|
||
recovered = self._health_monitor.trigger_recovery_and_wait(timeout=120)
|
||
if recovered:
|
||
logger.info("WDA 已恢复,继续等待安装完成...")
|
||
continue # 恢复成功,继续等待
|
||
else:
|
||
raise RuntimeError("WDA 恢复失败,安装等待中止") from e
|
||
else:
|
||
raise # 无 health_monitor 时原样上抛
|
||
|
||
if found:
|
||
logger.info(f"安装完成 (发现 '{label}' 按钮)")
|
||
|
||
# 通过对比 apps_before 和 apps_after 检测新安装的应用
|
||
if apps_before is not None:
|
||
apps_after = self._get_installed_apps_info()
|
||
if apps_after:
|
||
new_bundle_ids = set(apps_after.keys()) - set(apps_before.keys())
|
||
if len(new_bundle_ids) == 1:
|
||
bundle_id = list(new_bundle_ids)[0]
|
||
app_info = apps_after[bundle_id]
|
||
app_name = app_info.get('app_name', '')
|
||
version = app_info.get('version', '')
|
||
|
||
# 保存到CSV
|
||
self._save_app_mapping(app_id, bundle_id, app_name, version)
|
||
logger.info(f"Bundle ID (检测): {bundle_id}, 已保存到 CSV")
|
||
elif len(new_bundle_ids) == 0:
|
||
logger.warning("未检测到新安装的应用(可能是更新或重新安装)")
|
||
# 尝试从 CSV 缓存读取
|
||
app_mapping = self._load_app_mapping()
|
||
cached_info = app_mapping.get(app_id)
|
||
if cached_info and cached_info.get('bundle_id'):
|
||
logger.info(f"Bundle ID (缓存): {cached_info['bundle_id']}")
|
||
elif len(new_bundle_ids) > 1:
|
||
logger.warning(f"检测到多个新应用: {list(new_bundle_ids)}")
|
||
else:
|
||
logger.warning("未提供安装前应用列表,无法检测新安装的应用")
|
||
|
||
return True
|
||
|
||
time.sleep(2)
|
||
|
||
# 超时
|
||
logger.error(f"安装超时 ({timeout}秒)")
|
||
return False
|
||
|
||
def execute(self, app_id: str, timeout: float = 600.0) -> Tuple[bool, str]:
|
||
"""
|
||
执行安装流程
|
||
|
||
Args:
|
||
app_id: App Store应用ID
|
||
timeout: 安装超时时间(秒)
|
||
|
||
Returns:
|
||
Tuple[bool, str]: (应用是否成功安装在设备中, 失败原因说明)
|
||
"""
|
||
# 初始化
|
||
self._init_session()
|
||
app_id = self._format_app_id(app_id)
|
||
|
||
logger.info(f"开始安装应用: {app_id}")
|
||
|
||
try:
|
||
# 1. 检查WDA就绪
|
||
if not self._check_wda_ready():
|
||
return False, "WDA未就绪"
|
||
|
||
# 2. 打开App Store页面
|
||
if not self._open_appstore_page(app_id):
|
||
return False, "无法打开App Store页面"
|
||
|
||
# Alert 会在下次操作前自动处理
|
||
time.sleep(3) # 等待页面加载,确保弹窗有时间弹出
|
||
|
||
# 3. 先通过 Alert API 检查是否弹出"应用不可用"弹窗
|
||
if self._check_app_unavailable():
|
||
# 检测到不可用弹窗后主动 dismiss,避免残留弹窗干扰后续测试
|
||
self.popup_handler.handle_alert(strategy="allow")
|
||
return False, "应用不可用"
|
||
|
||
# 4. 通过 Alert API 检查网络状态弹窗
|
||
if not self._check_network():
|
||
self.popup_handler.handle_alert(strategy="allow")
|
||
return False, "网络未连接"
|
||
|
||
# 5. 检查是否已安装
|
||
if self._check_already_installed():
|
||
logger.info(f"应用已在设备中: {app_id}")
|
||
return True, ""
|
||
|
||
# 6. 检查是否为付费应用
|
||
is_paid, price = self._check_paid_app()
|
||
if is_paid:
|
||
return False, f"付费应用: {price}"
|
||
|
||
# 7. 获取安装前的应用列表(在点击获取按钮之前)
|
||
logger.debug("获取安装前应用列表")
|
||
apps_before = self._get_installed_apps_info()
|
||
if apps_before:
|
||
logger.debug(f"记录了 {len(apps_before)} 个已安装应用")
|
||
else:
|
||
logger.warning("无法获取已安装应用列表,将无法检测新安装的应用")
|
||
|
||
# 8. 点击获取按钮(含重试)
|
||
if not self._find_and_click_get_button():
|
||
return False, "未找到获取/下载按钮"
|
||
|
||
# 9. 点击确认安装(含重试),处理密码输入
|
||
confirm_clicked = self._confirm_install()
|
||
logger.debug(f"安装确认步骤结果: {'需要确认且已点击' if confirm_clicked else '无需确认或未找到按钮'}")
|
||
if confirm_clicked and (not self._handle_password_input()):
|
||
return False, "密码验证失败"
|
||
|
||
# 10. 等待安装完成(传入 apps_before)
|
||
if self._wait_for_installation(app_id, timeout, apps_before):
|
||
logger.info(f"应用已成功安装到设备: {app_id}")
|
||
return True, ""
|
||
else:
|
||
return False, f"安装超时 ({timeout}秒)"
|
||
|
||
except Exception as e:
|
||
logger.error(f"安装异常: {app_id} - {str(e)}")
|
||
return False, f"安装异常: {str(e)}"
|
||
|
||
|
||
def install_from_appstore(server_url: str, app_id: str, timeout: float = 600.0,
|
||
udid: Optional[str] = None,
|
||
ssh_host: Optional[str] = None,
|
||
go_ios: Optional[str] = 'ios',
|
||
alert_strategy: str = "allow",
|
||
health_monitor=None) -> Tuple[bool, str]:
|
||
"""
|
||
使用App Store安装应用
|
||
|
||
Args:
|
||
server_url: WDA Server URL
|
||
app_id: App Store应用ID
|
||
timeout: 安装超时时间(秒)
|
||
udid: 设备UDID
|
||
ssh_host: SSH主机地址
|
||
go_ios: go-ios可执行文件路径
|
||
alert_strategy: Alert 处理策略 - "allow" 或 "deny"
|
||
health_monitor: WDAHealthMonitor 实例(可选)
|
||
|
||
Returns:
|
||
Tuple[bool, str]: (应用是否成功安装在设备中, 失败原因说明)
|
||
"""
|
||
installer = AppStoreInstaller(server_url, udid, ssh_host, go_ios, alert_strategy, health_monitor=health_monitor)
|
||
return installer.execute(app_id, timeout)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# 使用示例
|
||
success, reason = install_from_appstore(
|
||
server_url="http://192.168.7.7:8100",
|
||
app_id="6523690380",
|
||
timeout=600,
|
||
ssh_host="nbt_sh_tplink@192.168.7.7",
|
||
go_ios='/Users/nbt_sh_tplink/Public/yfz/ios'
|
||
)
|
||
|
||
if success:
|
||
print("应用已成功安装到设备")
|
||
else:
|
||
print(f"应用安装失败: {reason}")
|