806 lines
34 KiB
Python
806 lines
34 KiB
Python
import logging
|
||
import time
|
||
import subprocess
|
||
import traceback
|
||
import socket
|
||
import os
|
||
import requests
|
||
from appium import webdriver
|
||
from appium.webdriver.common.appiumby import AppiumBy
|
||
from appium.webdriver.appium_service import AppiumService
|
||
from appium.options.android import UiAutomator2Options
|
||
from selenium.webdriver.support.ui import WebDriverWait
|
||
from selenium.webdriver.support import expected_conditions as EC
|
||
from selenium.common.exceptions import TimeoutException, NoSuchElementException, InvalidSessionIdException, WebDriverException
|
||
import globals.global_variable as global_variable
|
||
|
||
|
||
def init_appium_driver(device_id, app_package="com.bjjw.cjgc", app_activity=".activity.LoginActivity"):
|
||
"""
|
||
初始化Appium驱动的全局函数
|
||
|
||
参数:
|
||
device_id: 设备ID
|
||
app_package: 应用包名,默认为"com.bjjw.cjgc"
|
||
app_activity: 应用启动Activity,默认为".activity.LoginActivity"
|
||
|
||
返回:
|
||
(driver, wait): (WebDriver实例, WebDriverWait实例),如果初始化失败则抛出异常
|
||
"""
|
||
logging.info(f"设备 {device_id} 开始初始化Appium驱动")
|
||
|
||
# 创建并配置Appium选项
|
||
options = UiAutomator2Options()
|
||
options.platform_name = "Android"
|
||
options.device_name = device_id
|
||
options.app_package = app_package
|
||
options.app_activity = app_activity
|
||
options.automation_name = "UiAutomator2"
|
||
options.no_reset = True
|
||
options.auto_grant_permissions = True
|
||
options.new_command_timeout = 28800
|
||
options.udid = device_id
|
||
|
||
# 增加uiautomator2服务器启动超时时间
|
||
options.set_capability('uiautomator2ServerLaunchTimeout', 60000) # 60秒
|
||
# 增加连接超时设置
|
||
options.set_capability('connection_timeout', 120000) # 120秒
|
||
|
||
try:
|
||
# 连接Appium服务器
|
||
# driver_url = "http://127.0.0.1:4723/wd/hub"
|
||
# logging.info(f"设备 {device_id} 正在连接Appium服务器: {driver_url}")
|
||
# driver = webdriver.Remote(driver_url, options=options)
|
||
# logging.info(f"设备 {device_id} Appium服务器连接成功")
|
||
driver_urls = [
|
||
"http://127.0.0.1:4723/wd/hub", # 标准路径
|
||
"http://127.0.0.1:4723", # 简化路径
|
||
"http://localhost:4723/wd/hub", # localhost
|
||
]
|
||
|
||
driver = None
|
||
last_exception = None
|
||
|
||
# 尝试多个URL
|
||
for driver_url in driver_urls:
|
||
try:
|
||
logging.info(f"设备 {device_id} 正在连接Appium服务器: {driver_url}")
|
||
driver = webdriver.Remote(driver_url, options=options)
|
||
logging.info(f"设备 {device_id} Appium服务器连接成功: {driver_url}")
|
||
break # 连接成功,跳出循环
|
||
except Exception as e:
|
||
last_exception = e
|
||
logging.warning(f"设备 {device_id} 连接失败 {driver_url}: {str(e)[:100]}")
|
||
continue
|
||
|
||
# 检查是否连接成功
|
||
if not driver:
|
||
logging.error(f"设备 {device_id} 所有Appium服务器地址尝试失败")
|
||
logging.error(f"最后错误: {str(last_exception)}")
|
||
raise Exception(f"设备 {device_id} 无法连接到Appium服务器: {str(last_exception)}")
|
||
|
||
|
||
# 初始化等待对象
|
||
wait = WebDriverWait(driver, 20)
|
||
logging.info(f"设备 {device_id} WebDriverWait初始化成功")
|
||
|
||
# 等待应用稳定
|
||
time.sleep(2)
|
||
|
||
# 设置屏幕永不休眠
|
||
try:
|
||
# 使用ADB命令设置屏幕永不休眠
|
||
screen_timeout_cmd = [
|
||
"adb", "-s", device_id,
|
||
"shell", "settings", "put", "system", "screen_off_timeout", "86400000"
|
||
]
|
||
timeout_result = subprocess.run(screen_timeout_cmd, capture_output=True, text=True, timeout=15)
|
||
if timeout_result.returncode == 0:
|
||
logging.info(f"设备 {device_id} 已成功设置屏幕永不休眠")
|
||
else:
|
||
logging.warning(f"设备 {device_id} 设置屏幕永不休眠失败: {timeout_result.stderr}")
|
||
except Exception as timeout_error:
|
||
logging.warning(f"设备 {device_id} 设置屏幕永不休眠时出错: {str(timeout_error)}")
|
||
|
||
logging.info(f"设备 {device_id} Appium驱动初始化完成")
|
||
return driver, wait
|
||
|
||
except Exception as e:
|
||
logging.error(f"设备 {device_id} 初始化驱动失败: {str(e)}")
|
||
logging.error(f"错误类型: {type(e).__name__}")
|
||
logging.error(f"错误堆栈: {traceback.format_exc()}")
|
||
# 如果驱动已创建,尝试关闭
|
||
if 'driver' in locals() and driver:
|
||
try:
|
||
driver.quit()
|
||
except:
|
||
pass
|
||
raise
|
||
|
||
def check_session_valid(driver, device_id=None):
|
||
"""
|
||
检查当前会话是否有效
|
||
|
||
参数:
|
||
driver: WebDriver实例
|
||
device_id: 设备ID(可选)
|
||
|
||
返回:
|
||
bool: 会话有效返回True,否则返回False
|
||
"""
|
||
if device_id is None:
|
||
device_id = global_variable.get_device_id()
|
||
device_str = f"设备 {device_id} " if device_id else ""
|
||
|
||
if not driver:
|
||
logging.debug(f"{device_str}驱动实例为空")
|
||
return False
|
||
|
||
try:
|
||
# 首先检查driver是否有session_id属性
|
||
if not hasattr(driver, 'session_id') or not driver.session_id:
|
||
logging.debug(f"{device_str}驱动缺少有效的session_id")
|
||
return False
|
||
|
||
# 尝试获取当前上下文
|
||
current_context = driver.current_context
|
||
logging.debug(f"{device_str}会话检查通过,当前上下文: {current_context}")
|
||
return True
|
||
|
||
except InvalidSessionIdException:
|
||
logging.debug(f"{device_str}会话已失效")
|
||
return False
|
||
|
||
except WebDriverException as e:
|
||
error_msg = str(e).lower()
|
||
|
||
# 明确的会话失效错误
|
||
if any(phrase in error_msg for phrase in [
|
||
"session is either terminated or not started",
|
||
"could not proxy command to the remote server",
|
||
"socket hang up",
|
||
"connection refused",
|
||
"max retries exceeded"
|
||
]):
|
||
logging.debug(f"{device_str}会话连接错误: {error_msg[:100]}")
|
||
return False
|
||
else:
|
||
logging.debug(f"{device_str}WebDriver异常但可能不是会话失效: {error_msg[:100]}")
|
||
return True
|
||
|
||
except (ConnectionError, ConnectionRefusedError, ConnectionResetError) as e:
|
||
logging.debug(f"{device_str}网络连接错误: {str(e)}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
error_msg = str(e)
|
||
# 检查是否是连接相关错误
|
||
if any(phrase in error_msg.lower() for phrase in [
|
||
"10054", "10061", "connection", "connect", "refused", "urllib3"
|
||
]):
|
||
logging.debug(f"{device_str}连接相关异常: {error_msg[:100]}")
|
||
return False
|
||
else:
|
||
logging.debug(f"{device_str}检查会话时出现其他异常: {error_msg[:100]}")
|
||
return True # 对于真正的未知异常,保守返回True
|
||
|
||
|
||
def reconnect_driver(device_id, old_driver=None, app_package="com.bjjw.cjgc", app_activity=".activity.LoginActivity"):
|
||
"""
|
||
重新连接Appium驱动,不重新启动应用
|
||
|
||
参数:
|
||
device_id: 设备ID
|
||
old_driver: 旧的WebDriver实例(可选)
|
||
app_package: 应用包名
|
||
app_activity: 应用启动Activity
|
||
|
||
返回:
|
||
(driver, wait): 新的WebDriver和WebDriverWait实例
|
||
"""
|
||
# 使用传入的device_id或从全局变量获取
|
||
if not device_id:
|
||
device_id = global_variable.get_device_id()
|
||
|
||
# 修复device_id参数类型问题并使用全局设备ID作为备用
|
||
actual_device_id = device_id
|
||
|
||
# 检查device_id是否为有效的字符串格式
|
||
if not actual_device_id or (isinstance(actual_device_id, str) and ("session=" in actual_device_id or len(actual_device_id.strip()) == 0)):
|
||
# 尝试从old_driver获取设备ID
|
||
if old_driver and hasattr(old_driver, 'capabilities'):
|
||
capability_device_id = old_driver.capabilities.get('udid')
|
||
if capability_device_id:
|
||
actual_device_id = capability_device_id
|
||
logging.warning(f"检测到device_id参数无效,已从old_driver中提取设备ID: {actual_device_id}")
|
||
|
||
# 如果仍然没有有效的设备ID,使用全局变量
|
||
if not actual_device_id or (isinstance(actual_device_id, str) and ("session=" in actual_device_id or len(actual_device_id.strip()) == 0)):
|
||
actual_device_id = global_variable.get_device_id()
|
||
logging.warning(f"无法获取有效设备ID,使用全局变量GLOBAL_DEVICE_ID: {actual_device_id}")
|
||
|
||
device_id = actual_device_id # 使用修正后的设备ID
|
||
logging.info(f"设备 {device_id} 开始重新连接驱动(不重启应用)")
|
||
|
||
# # 首先安全关闭旧驱动
|
||
# if old_driver:
|
||
# safe_quit_driver(old_driver, device_id)
|
||
|
||
max_reconnect_attempts = 3
|
||
reconnect_delay = 5 # 秒
|
||
|
||
for attempt in range(max_reconnect_attempts):
|
||
try:
|
||
logging.info(f"设备 {device_id} 第{attempt + 1}次尝试重新连接")
|
||
|
||
# 确保Appium服务器运行
|
||
if not ensure_appium_server_running():
|
||
logging.warning(f"设备 {device_id} Appium服务器未运行,尝试启动")
|
||
time.sleep(reconnect_delay)
|
||
continue
|
||
|
||
# 创建并配置Appium选项 - 重点:设置 autoLaunch=False 不自动启动应用
|
||
options = UiAutomator2Options()
|
||
options.platform_name = "Android"
|
||
options.device_name = device_id
|
||
options.app_package = app_package
|
||
options.app_activity = app_activity
|
||
options.automation_name = "UiAutomator2"
|
||
options.no_reset = True
|
||
options.auto_grant_permissions = True
|
||
options.new_command_timeout = 3600
|
||
options.udid = device_id
|
||
|
||
# 关键设置:不自动启动应用
|
||
options.set_capability('autoLaunch', False)
|
||
options.set_capability('skipUnlock', True)
|
||
options.set_capability('skipServerInstallation', True)
|
||
options.set_capability('skipDeviceInitialization', True)
|
||
|
||
# 增加uiautomator2服务器启动超时时间
|
||
options.set_capability('uiautomator2ServerLaunchTimeout', 60000) # 60秒
|
||
# 增加连接超时设置
|
||
options.set_capability('connection_timeout', 120000) # 120秒
|
||
|
||
# 连接Appium服务器
|
||
logging.info(f"设备 {device_id} 正在连接Appium服务器(不启动应用)")
|
||
driver = webdriver.Remote("http://localhost:4723", options=options)
|
||
logging.info(f"设备 {device_id} Appium服务器连接成功")
|
||
|
||
# 初始化等待对象
|
||
wait = WebDriverWait(driver, 20)
|
||
logging.info(f"设备 {device_id} WebDriverWait初始化成功")
|
||
|
||
# 不启动应用,直接附加到当前运行的应用
|
||
try:
|
||
# 获取当前运行的应用
|
||
current_package = driver.current_package
|
||
logging.info(f"设备 {device_id} 当前运行的应用: {current_package}")
|
||
|
||
# 如果当前运行的不是目标应用,尝试切换到目标应用
|
||
if current_package != app_package:
|
||
logging.info(f"设备 {device_id} 当前应用不是目标应用,尝试启动目标应用")
|
||
launch_app_manually(driver, app_package, app_activity)
|
||
else:
|
||
logging.info(f"设备 {device_id} 已成功连接到运行中的目标应用")
|
||
except Exception as attach_error:
|
||
logging.warning(f"设备 {device_id} 获取当前应用信息失败: {str(attach_error)}")
|
||
# 即使获取当前应用失败,也继续使用连接
|
||
|
||
# 验证新会话是否有效
|
||
if check_session_valid(driver, device_id):
|
||
logging.info(f"设备 {device_id} 重新连接成功")
|
||
return driver, wait
|
||
else:
|
||
logging.warning(f"设备 {device_id} 新创建的会话无效,将重试")
|
||
safe_quit_driver(driver, device_id)
|
||
|
||
except Exception as e:
|
||
logging.error(f"设备 {device_id} 第{attempt + 1}次重新连接失败: {str(e)}")
|
||
if attempt < max_reconnect_attempts - 1:
|
||
wait_time = reconnect_delay * (attempt + 1)
|
||
logging.info(f"设备 {device_id} 将在{wait_time}秒后重试重新连接")
|
||
time.sleep(wait_time)
|
||
else:
|
||
logging.error(f"设备 {device_id} 所有重新连接尝试均失败")
|
||
# 所有尝试都失败后才关闭旧驱动
|
||
if old_driver:
|
||
safe_quit_driver(old_driver, device_id)
|
||
raise
|
||
|
||
# 所有尝试都失败
|
||
raise Exception(f"设备 {device_id} 重新连接失败,已尝试{max_reconnect_attempts}次")
|
||
|
||
|
||
|
||
# def ensure_appium_server_running(port=4723):
|
||
# """使用完整的环境变量启动Appium,解决路径缺失问题"""
|
||
# try:
|
||
# # 获取当前用户的环境变量
|
||
# env = os.environ.copy()
|
||
|
||
# # ========== 仅修改这部分:Windows下Node.js/Appium常见路径 ==========
|
||
# additional_paths = [
|
||
# # Windows默认Node.js安装路径(64位)
|
||
# os.path.join("C:\\", "Program Files\\nodejs"),
|
||
# # Windows32位Node.js路径
|
||
# os.path.join("C:\\", "Program Files (x86)\\nodejs"),
|
||
# # npm全局安装路径(Windows核心,appium一般装在这里)
|
||
# os.path.expanduser("~\\AppData\\Roaming\\npm"),
|
||
# # 系统默认路径(防止基础命令缺失)
|
||
# os.path.join("C:\\", "Windows\\System32"),
|
||
# os.path.join("C:\\", "Windows\\System"),
|
||
# # 自定义Node.js/npm路径(可选,根据你的实际安装路径加)
|
||
# # "D:\\Program Files\\nodejs", # 若你装在D盘,解开注释并修改
|
||
# ]
|
||
# # ========== Windows路径修改结束 ==========
|
||
|
||
# # 更新PATH环境变量(跨系统通用,os.pathsep自动适配Windows的;和macOS的:)
|
||
# current_path = env.get('PATH', '')
|
||
# new_path = current_path + os.pathsep + os.pathsep.join(additional_paths)
|
||
# env['PATH'] = new_path
|
||
|
||
# # 构建启动命令(和原函数一致,无修改)
|
||
# appium_cmd = f"appium -p {port} --log-level error"
|
||
|
||
# # 使用完整环境启动(跨系统通用,Windows下正常执行)
|
||
# process = subprocess.Popen(
|
||
# appium_cmd,
|
||
# shell=True, # Windows下字符串命令必须开启,和macOS一致
|
||
# env=env, # 传入补全后的Windows环境变量(核心)
|
||
# stdout=subprocess.PIPE, # 捕获输出,控制台不刷屏
|
||
# stderr=subprocess.PIPE,
|
||
# text=True # 输出为字符串,无需手动解码(跨系统通用)
|
||
# )
|
||
|
||
# logging.info(f"Appium启动进程已创建,PID: {process.pid}")
|
||
# # 等待并校验服务启动成功(需确保wait_for_appium_start已定义,timeout有值)
|
||
# return wait_for_appium_start(port)
|
||
|
||
# except Exception as e:
|
||
# logging.error(f"Windows下启动Appium时出错: {str(e)}")
|
||
# return False
|
||
|
||
def is_port_in_use(port):
|
||
"""检查端口是否被占用"""
|
||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||
return s.connect_ex(('127.0.0.1', port)) == 0
|
||
|
||
def kill_system_process(process_name):
|
||
"""杀掉系统进程"""
|
||
try:
|
||
# Windows
|
||
subprocess.run(f"taskkill /F /IM {process_name}", shell=True, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
|
||
except Exception:
|
||
pass
|
||
|
||
def start_appium_server(port=4723):
|
||
"""启动 Appium 服务,强制指定路径兼容性"""
|
||
# 1. 先尝试清理可能占用的 node 进程
|
||
if is_port_in_use(port):
|
||
logging.warning(f"端口 {port} 被占用,尝试清理 node.exe...")
|
||
kill_system_process("node.exe")
|
||
time.sleep(2)
|
||
|
||
# 2. 构造启动命令
|
||
# 注意:这里增加了 --base-path /wd/hub 解决 404 问题
|
||
# --allow-cors 允许跨域,有时候能解决连接问题
|
||
appium_cmd = f"appium -p {port} --base-path /wd/hub --allow-cors"
|
||
|
||
logging.info(f"正在启动 Appium: {appium_cmd}")
|
||
try:
|
||
# 使用 shell=True 在 Windows 上更稳定
|
||
# creationflags=subprocess.CREATE_NEW_CONSOLE 可以让它在后台运行不弹出窗口
|
||
subprocess.Popen(appium_cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||
logging.info("Appium 启动命令已发送,等待服务就绪...")
|
||
except Exception as e:
|
||
logging.error(f"启动 Appium 进程失败: {e}")
|
||
|
||
def check_server_status(port):
|
||
"""检测服务器状态,兼容 Appium 1.x 和 2.x 路径"""
|
||
base_url = f"http://127.0.0.1:{port}"
|
||
check_paths = ["/wd/hub/status", "/status"] # 优先检查 /wd/hub
|
||
|
||
for path in check_paths:
|
||
try:
|
||
url = f"{base_url}{path}"
|
||
response = requests.get(url, timeout=1)
|
||
if response.status_code == 200:
|
||
return True
|
||
except:
|
||
pass
|
||
return False
|
||
|
||
def ensure_appium_server_running(port=4723):
|
||
"""确保 Appium 服务器正在运行,如果没运行则启动它"""
|
||
|
||
# 1. 第一次快速检测
|
||
if check_server_status(port):
|
||
logging.info(f"Appium 服务已在端口 {port} 运行")
|
||
return True
|
||
|
||
# 2. 如果没运行,启动它
|
||
logging.warning(f"Appium 未在端口 {port} 运行,准备启动...")
|
||
start_appium_server(port)
|
||
|
||
# 3. 循环等待启动成功(最多等待 20 秒)
|
||
max_retries = 20
|
||
for i in range(max_retries):
|
||
if check_server_status(port):
|
||
logging.info("Appium 服务启动成功并已就绪!")
|
||
return True
|
||
|
||
time.sleep(1)
|
||
if i % 5 == 0:
|
||
logging.info(f"等待 Appium 启动中... ({i}/{max_retries})")
|
||
|
||
logging.error("Appium 服务启动超时!请检查 appium 命令是否在命令行可直接运行。")
|
||
return False
|
||
|
||
|
||
def wait_for_appium_start(port, timeout=10):
|
||
"""
|
||
检测指定端口的Appium服务是否真正启动并可用
|
||
:param port: Appium服务端口
|
||
:param timeout: 最大等待时间(秒)
|
||
:return: 服务就绪返回True,超时/失败返回False
|
||
"""
|
||
# Appium官方状态查询接口
|
||
check_url = f"http://localhost:{port}/wd/hub/status"
|
||
# 检测开始时间
|
||
start_check_time = time.time()
|
||
logging.info(f"开始检测Appium服务是否就绪,端口:{port},最大等待{timeout}秒")
|
||
|
||
while time.time() - start_check_time < timeout:
|
||
try:
|
||
# 发送HTTP请求,超时1秒(避免单次检测卡太久)
|
||
response = requests.get(
|
||
url=check_url,
|
||
timeout=1,
|
||
verify=False # 本地接口,禁用SSL验证
|
||
)
|
||
# 接口返回200(HTTP成功状态码),且JSON中status=0(Appium服务正常)
|
||
if response.status_code == 200 and response.json().get("status") == 0:
|
||
logging.info(f"Appium服务检测成功,端口{port}已就绪")
|
||
return True
|
||
except Exception as e:
|
||
# 捕获所有异常(连接拒绝、超时、JSON解析失败等),说明服务未就绪
|
||
logging.debug(f"本次检测Appium服务未就绪:{str(e)}") # 调试日志,不刷屏
|
||
|
||
# 检测失败,休眠1秒后重试
|
||
time.sleep(1)
|
||
|
||
# 循环结束→超时
|
||
logging.error(f"检测超时!{timeout}秒内Appium服务端口{port}仍未就绪")
|
||
return False
|
||
|
||
def safe_quit_driver(driver, device_id=None):
|
||
"""
|
||
安全关闭驱动的全局函数
|
||
|
||
参数:
|
||
driver: WebDriver实例
|
||
device_id: 设备ID(可选)
|
||
"""
|
||
if device_id is None:
|
||
device_id = global_variable.get_device_id()
|
||
device_str = f"设备 {device_id} " if device_id else ""
|
||
logging.info(f"{device_str}开始关闭驱动")
|
||
|
||
if not driver:
|
||
logging.info(f"{device_str}没有可关闭的驱动实例")
|
||
return
|
||
|
||
# 检查driver是否为WebDriver实例或是否有quit方法
|
||
if not hasattr(driver, 'quit'):
|
||
logging.warning(f"{device_str}驱动对象类型无效,不具有quit方法: {type(driver).__name__}")
|
||
return
|
||
|
||
max_quit_attempts = 3
|
||
for attempt in range(max_quit_attempts):
|
||
try:
|
||
logging.info(f"{device_str}尝试关闭驱动 (尝试 {attempt + 1}/{max_quit_attempts})")
|
||
driver.quit()
|
||
logging.info(f"{device_str}驱动已成功关闭")
|
||
return
|
||
except Exception as e:
|
||
logging.error(f"{device_str}关闭驱动时出错 (尝试 {attempt + 1}/{max_quit_attempts}): {str(e)}")
|
||
if attempt < max_quit_attempts - 1:
|
||
# 等待一段时间后重试
|
||
wait_time = 2
|
||
logging.info(f"{device_str}将在 {wait_time} 秒后重试")
|
||
time.sleep(wait_time)
|
||
else:
|
||
logging.critical(f"{device_str}尝试多次关闭驱动失败,可能导致资源泄漏")
|
||
|
||
|
||
def is_app_launched(driver, package_name="com.bjjw.cjgc"):
|
||
"""
|
||
检查应用是否已启动
|
||
|
||
参数:
|
||
driver: WebDriver实例
|
||
package_name: 应用包名,默认为"com.bjjw.cjgc"
|
||
|
||
返回:
|
||
bool: 如果应用已启动则返回True,否则返回False
|
||
"""
|
||
try:
|
||
# 通过检查当前活动的包名来确认应用是否已启动
|
||
current_package = driver.current_package
|
||
return current_package == package_name
|
||
except Exception as e:
|
||
logging.error(f"检查应用启动状态时出错: {str(e)}")
|
||
return False
|
||
|
||
|
||
def launch_app_manually(driver, device_id, package_name="com.bjjw.cjgc", activity=".activity.LoginActivity"):
|
||
"""
|
||
手动启动应用
|
||
|
||
参数:
|
||
driver: WebDriver实例
|
||
package_name: 应用包名,默认为"com.bjjw.cjgc"
|
||
activity: 应用启动Activity,默认为".activity.LoginActivity"
|
||
"""
|
||
try:
|
||
if not device_id:
|
||
device_id = global_variable.get_device_id()
|
||
# 尝试从driver获取设备ID
|
||
if driver and hasattr(driver, 'capabilities'):
|
||
device_id = driver.capabilities.get('udid')
|
||
device_str = f"设备 {device_id} " if device_id else ""
|
||
else:
|
||
device_str = ""
|
||
|
||
logging.info(f"{device_str}尝试手动启动应用: {package_name}/{activity}")
|
||
|
||
# 首先使用ADB命令退出应用
|
||
if device_id:
|
||
try:
|
||
# 使用ADB命令强制停止应用
|
||
stop_cmd = [
|
||
"adb", "-s", device_id,
|
||
"shell", "am", "force-stop",
|
||
package_name
|
||
]
|
||
stop_result = subprocess.run(stop_cmd, capture_output=True, text=True, timeout=15)
|
||
if stop_result.returncode == 0:
|
||
logging.info(f"{device_str}已使用ADB命令成功退出应用")
|
||
else:
|
||
logging.warning(f"{device_str}ADB退出应用失败: {stop_result.stderr}")
|
||
except Exception as stop_error:
|
||
logging.warning(f"{device_str}退出应用时出错: {str(stop_error)}")
|
||
|
||
# 首先尝试使用driver的execute_script方法启动应用
|
||
try:
|
||
if driver:
|
||
driver.execute_script("mobile: startActivity", {
|
||
"intent": f"{package_name}/{activity}"
|
||
})
|
||
logging.info(f"{device_str}已使用Appium startActivity命令启动应用")
|
||
except Exception as inner_e:
|
||
logging.warning(f"{device_str}使用Appium startActivity命令失败: {str(inner_e)},尝试使用ADB命令")
|
||
|
||
# 如果device_id可用,使用ADB命令启动应用
|
||
if device_id:
|
||
cmd = [
|
||
"adb", "-s", device_id,
|
||
"shell", "am", "start",
|
||
"-n", f"{package_name}/{activity}"
|
||
]
|
||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
||
|
||
if result.returncode == 0:
|
||
logging.info(f"{device_str}已使用ADB命令成功启动应用")
|
||
else:
|
||
logging.error(f"{device_str}ADB启动应用失败: {result.stderr}")
|
||
else:
|
||
logging.warning("无法获取设备ID,无法使用ADB命令启动应用")
|
||
|
||
# 设置屏幕永不休眠
|
||
if device_id:
|
||
try:
|
||
# 使用ADB命令设置屏幕永不休眠
|
||
screen_timeout_cmd = [
|
||
"adb", "-s", device_id,
|
||
"shell", "settings", "put", "system", "screen_off_timeout", "0"
|
||
]
|
||
timeout_result = subprocess.run(screen_timeout_cmd, capture_output=True, text=True, timeout=15)
|
||
if timeout_result.returncode == 0:
|
||
logging.info(f"{device_str}已成功设置屏幕永不休眠")
|
||
else:
|
||
logging.warning(f"{device_str}设置屏幕永不休眠失败: {timeout_result.stderr}")
|
||
except Exception as timeout_error:
|
||
logging.warning(f"{device_str}设置屏幕永不休眠时出错: {str(timeout_error)}")
|
||
|
||
# 等待应用启动
|
||
time.sleep(5)
|
||
except Exception as e:
|
||
logging.error(f"手动启动应用时出错: {str(e)}")
|
||
logging.error(f"错误堆栈: {traceback.format_exc()}")
|
||
|
||
def go_main_click_tabber_button(driver, device_id, tabber_button_text, max_retries=3):
|
||
"""
|
||
跳转到主页面并点击对应的导航菜单按钮(带重试机制)
|
||
|
||
参数:
|
||
driver: WebDriver实例
|
||
device_id: 设备ID
|
||
tabber_button_text: 导航菜单按钮的文本
|
||
max_retries: 最大重试次数
|
||
|
||
返回:
|
||
bool: 成功返回True,失败返回False
|
||
"""
|
||
retry_count = 0
|
||
|
||
while retry_count < max_retries:
|
||
try:
|
||
logging.info(f"设备 {device_id} 第 {retry_count + 1} 次尝试执行导航操作")
|
||
|
||
if not check_session_valid(driver, device_id):
|
||
logging.warning(f"设备 {device_id} 会话无效,尝试重新连接驱动...")
|
||
try:
|
||
# 重新连接,获取新的driver
|
||
new_driver, _ = reconnect_driver(device_id, driver)
|
||
driver = new_driver # 更新driver引用
|
||
logging.info(f"设备 {device_id} 驱动重连成功")
|
||
except Exception as e:
|
||
logging.error(f"设备 {device_id} 驱动重连失败: {str(e)}")
|
||
retry_count += 1
|
||
if retry_count < max_retries:
|
||
time.sleep(2)
|
||
continue
|
||
else:
|
||
return False
|
||
|
||
# 检查当前是否已经在主页面
|
||
current_activity = driver.current_activity
|
||
logging.info(f"设备 {device_id} 当前Activity: {current_activity}")
|
||
|
||
if ".activity.MainActivity" in current_activity:
|
||
logging.info(f"设备 {device_id} 已在主页面")
|
||
else:
|
||
logging.info(f"设备 {device_id} 当前不在主页面,循环点击返回按钮,直到在主页面")
|
||
|
||
max_back_presses = 10 # 最大返回键次数
|
||
back_press_count = 0
|
||
|
||
while ".activity.MainActivity" not in current_activity and back_press_count < max_back_presses:
|
||
try:
|
||
if not check_session_valid(driver, device_id):
|
||
logging.warning(f"设备 {device_id} 会话无效,尝试重新连接驱动...")
|
||
if not reconnect_driver(device_id, driver):
|
||
logging.error(f"设备 {device_id} 驱动重连失败")
|
||
# 点击返回按钮
|
||
driver.back()
|
||
back_press_count += 1
|
||
time.sleep(1)
|
||
|
||
# 更新当前Activity
|
||
current_activity = driver.current_activity
|
||
logging.info(f"设备 {device_id} 点击返回按钮 {back_press_count} 次后,当前Activity: {current_activity}")
|
||
|
||
except Exception as inner_e:
|
||
logging.warning(f"设备 {device_id} 点击返回按钮时出错: {str(inner_e)}")
|
||
break
|
||
|
||
# 检查是否成功回到主页面
|
||
if ".activity.MainActivity" not in current_activity:
|
||
logging.warning(f"设备 {device_id} 无法回到主页面,当前Activity: {current_activity}")
|
||
# 不立即返回,继续重试逻辑
|
||
retry_count += 1
|
||
if retry_count < max_retries:
|
||
logging.info(f"设备 {device_id} 等待2秒后重试...")
|
||
time.sleep(2)
|
||
continue
|
||
else:
|
||
logging.error(f"设备 {device_id} 达到最大重试次数,无法回到主页面")
|
||
return False
|
||
|
||
# 现在已经在主页面,点击指定的导航菜单按钮
|
||
# logging.info(f"设备 {device_id} 已在主页面,尝试点击导航菜单按钮: {tabber_button_text}")
|
||
|
||
try:
|
||
tabber_button = driver.find_element(AppiumBy.ID, tabber_button_text)
|
||
# 点击按钮
|
||
tabber_button.click()
|
||
tabber_button.click()
|
||
logging.info(f"设备 {device_id} 已成功点击导航菜单按钮: {tabber_button_text}")
|
||
|
||
# 等待页面加载
|
||
time.sleep(2)
|
||
|
||
# 验证操作是否成功
|
||
# 可以添加一些验证逻辑,比如检查是否跳转到目标页面
|
||
new_activity = driver.current_activity
|
||
logging.info(f"设备 {device_id} 点击后当前Activity: {new_activity}")
|
||
|
||
return True
|
||
|
||
except TimeoutException:
|
||
logging.error(f"设备 {device_id} 等待导航菜单按钮 '{tabber_button_text}' 超时")
|
||
except Exception as e:
|
||
logging.error(f"设备 {device_id} 点击导航菜单按钮 '{tabber_button_text}' 时出错: {str(e)}")
|
||
|
||
# 检查会话有效性并尝试重连
|
||
if not check_session_valid(driver, device_id):
|
||
logging.warning(f"设备 {device_id} 会话无效,尝试重新连接驱动...")
|
||
if reconnect_driver(device_id, driver):
|
||
logging.info(f"设备 {device_id} 驱动重连成功,继续重试")
|
||
# 重连成功后继续循环
|
||
retry_count += 1
|
||
if retry_count < max_retries:
|
||
time.sleep(2)
|
||
continue
|
||
else:
|
||
logging.error(f"设备 {device_id} 驱动重连失败")
|
||
return False
|
||
else:
|
||
# 会话有效但点击失败,可能是页面元素问题
|
||
logging.warning(f"设备 {device_id} 会话有效但点击失败,可能是页面加载问题")
|
||
|
||
# 如果点击按钮失败,增加重试计数
|
||
retry_count += 1
|
||
if retry_count < max_retries:
|
||
logging.info(f"设备 {device_id} 点击按钮失败,等待2秒后第 {retry_count + 1} 次重试...")
|
||
time.sleep(2)
|
||
else:
|
||
logging.error(f"设备 {device_id} 达到最大重试次数 {max_retries},导航失败")
|
||
return False
|
||
|
||
except Exception as e:
|
||
logging.error(f"设备 {device_id} 第 {retry_count + 1} 次尝试时出错: {str(e)}")
|
||
retry_count += 1
|
||
if retry_count < max_retries:
|
||
logging.info(f"设备 {device_id} 等待2秒后重试...")
|
||
time.sleep(2)
|
||
else:
|
||
logging.error(f"设备 {device_id} 达到最大重试次数 {max_retries},导航失败")
|
||
return False
|
||
|
||
return False
|
||
|
||
def check_connection_error(exception):
|
||
"""检查是否为连接拒绝错误"""
|
||
error_str = str(exception)
|
||
connection_errors = [
|
||
'远程主机强迫关闭了一个现有的连接',
|
||
'由于目标计算机积极拒绝,无法连接',
|
||
'ConnectionResetError',
|
||
'NewConnectionError',
|
||
'10054',
|
||
'10061'
|
||
]
|
||
return any(error in error_str for error in connection_errors)
|
||
|
||
def restart_appium_server(port=4723):
|
||
"""重启Appium服务器"""
|
||
try:
|
||
# 杀死可能存在的Appium进程
|
||
subprocess.run(['taskkill', '/f', '/im', 'node.exe'],
|
||
capture_output=True, shell=True)
|
||
time.sleep(2)
|
||
|
||
# 启动Appium服务器
|
||
# appium_command = f'appium -p {port}'
|
||
appium_command = f"appium -p {port} --base-path /wd/hub"
|
||
subprocess.Popen(appium_command, shell=True)
|
||
|
||
# 等待Appium启动
|
||
time.sleep(10)
|
||
return True
|
||
except Exception as e:
|
||
print(f"重启Appium服务器失败: {str(e)}")
|
||
return False
|
||
|
||
def is_appium_running(port=4723):
|
||
"""检查Appium服务器是否在运行"""
|
||
try:
|
||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||
result = s.connect_ex(('127.0.0.1', port))
|
||
return result == 0
|
||
except:
|
||
return False
|