Files
cjgc_data/globals/driver_utils.py
2026-03-14 17:53:14 +08:00

1292 lines
55 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s: %(message)s")
class DriverManager:
_driver = None
_wait = None
_device_id = None
@classmethod
def get_driver(cls):
# 如果驱动不存在,或者 session 失效,则初始化/重连
if cls._device_id is None:
cls._device_id = get_device_id()
if grant_appium_permissions(cls._device_id):
logging.info(f"设备 {cls._device_id} 授予Appium权限成功")
else:
logging.warning(f"设备 {cls._device_id} 授予Appium权限失败")
if not check_server_status(4723):
start_appium_server()
if cls._driver is None:
cls._driver, cls._wait = init_appium_driver(cls._device_id)
elif not check_session_valid(cls._driver, cls._device_id):
logging.info("检测到 Session 失效,正在重连...")
cls._driver, cls._wait = reconnect_driver(cls._device_id, cls._driver)
return cls._driver, cls._wait, cls._device_id
@classmethod
def quit_driver(cls, device_id):
if cls._driver:
safe_quit_driver(cls._driver, device_id)
cls._driver = None
cls._wait = None
cls._device_id = None
def get_device_id() -> str:
"""
获取设备ID优先使用已连接设备否则使用全局配置
"""
try:
# 检查已连接设备
result = subprocess.run(
["adb", "devices"],
capture_output=True,
text=True,
timeout=10
)
target_port = "4723"
for line in result.stdout.strip().split('\n')[1:]:
if line.strip() and "device" in line and "offline" not in line:
device_id = line.split('\t')[0]
# 检查是否为无线设备且端口为4723
if ':' in device_id:
ip_port = device_id.split(':')
if len(ip_port) == 2 and ip_port[1] == target_port:
logging.info(f"找到目标无线设备(端口{target_port}): {device_id}")
global_variable.GLOBAL_DEVICE_ID = device_id
return device_id
# 如果没有找到端口4723的设备找其他无线设备
for line in result.stdout.strip().split('\n')[1:]:
if line.strip() and "device" in line and "offline" not in line:
device_id = line.split('\t')[0]
# 检查是否为无线设备(任何端口)
if ':' in device_id and device_id.split(':')[-1].isdigit():
logging.info(f"未找到端口{target_port}的设备,使用其他无线设备: {device_id}")
global_variable.GLOBAL_DEVICE_ID = device_id
return device_id
# 如果没有任何无线设备,找有线设备
for line in result.stdout.strip().split('\n')[1:]:
if line.strip() and "device" in line and "offline" not in line:
device_id = line.split('\t')[0]
logging.info(f"未找到无线设备,使用有线设备: {device_id}")
global_variable.GLOBAL_DEVICE_ID = device_id
return device_id
logging.error("未找到任何可用设备")
return None
except Exception as e:
logging.warning(f"设备检测失败: {e}")
return None
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 = 3600
options.udid = device_id
# 增加uiautomator2服务器启动超时时间
options.set_capability('uiautomator2ServerLaunchTimeout', 60000) # 60秒
# 增加连接超时设置
options.set_capability('connection_timeout', 120000) # 120秒
# # 添加额外的能力,避免初始化错误
# options.set_capability('skipDeviceInitialization', True) # 跳过设备初始化,避免修改系统设置
# options.set_capability('disableHiddenApiPolicy', True) # 禁用隐藏API策略避免权限问题
# options.set_capability('skipServerInstallation', True) # 跳过服务器安装使用已有的UiAutomator2服务器
# options.set_capability('skipUnlock', True) # 跳过解锁屏幕,避免干扰
try:
# driver_url = "http://127.0.0.1:4723/wd/hub"
# # 连接Appium服务器
# 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
"""
# 从全局变量获取设备ID
if device_id is None:
device_id = global_variable.GLOBAL_DEVICE_ID
device_str = f"设备 {device_id} " if device_id else ""
if not driver:
logging.warning(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.error(f"{device_str}会话已失效 (InvalidSessionIdException)")
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.GLOBAL_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.GLOBAL_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服务器
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初始化成功")
# 不启动应用,直接附加到当前运行的应用
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()
# # 添加常见的Node.js路径macOS常见问题
# additional_paths = [
# "/usr/local/bin",
# "/opt/homebrew/bin", # Apple Silicon Mac
# "/usr/bin",
# "/bin",
# os.path.expanduser("~/.npm-global/bin"),
# os.path.expanduser("~/node_modules/.bin"),
# # Android基础路径
# "/Users/{}/Library/Android/sdk/platform-tools".format(os.getenv('USER')),
# ]
# # 更新PATH环境变量
# 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"
# # 使用完整环境启动
# process = subprocess.Popen(
# appium_cmd,
# shell=True,
# env=env,
# stdout=subprocess.PIPE,
# stderr=subprocess.PIPE,
# text=True
# )
# logging.info(f"Appium启动进程已创建PID: {process.pid}")
# return wait_for_appium_start(port)
# except Exception as e:
# logging.error(f"使用完整环境启动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 grant_appium_permissions(device_id: str, require_all: bool = False) -> bool:
"""
修复版:为 Appium 授予权限(使用正确的方法)
"""
logging.info(f"设备 {device_id}开始设置Appium权限")
# 1. 使用系统设置命令替代原来的pm grant尝试
logging.info("使用系统设置命令...")
system_commands = [
["adb", "-s", device_id, "shell", "settings", "put", "global", "window_animation_scale", "0"],
["adb", "-s", device_id, "shell", "settings", "put", "global", "transition_animation_scale", "0"],
["adb", "-s", device_id, "shell", "settings", "put", "global", "animator_duration_scale", "0"],
["adb", "-s", device_id, "shell", "settings", "put", "system", "screen_off_timeout", "86400000"],
]
success_count = 0
for cmd in system_commands:
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
success_count += 1
logging.info(f" 成功: {' '.join(cmd[3:])}")
else:
logging.warning(f" 失败: {' '.join(cmd[3:])}")
except:
logging.warning(f" 异常: {' '.join(cmd[3:])}")
# 2. 授予可自动授予的权限
logging.info("授予基础权限...")
grantable = [
"android.permission.INTERNET",
"android.permission.ACCESS_NETWORK_STATE",
"android.permission.ACCESS_WIFI_STATE",
]
for perm in grantable:
cmd = ["adb", "-s", device_id, "shell", "pm", "grant", "io.appium.settings", perm]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
success_count += 1
logging.info(f" 成功授予: {perm.split('.')[-1]}")
else:
logging.debug(f" 跳过: {perm.split('.')[-1]}")
# 3. 返回结果
logging.info(f"设置完成,成功项数: {success_count}")
if require_all:
return success_count == (len(system_commands) + len(grantable))
else:
return success_count > 0 # 只要有成功项就返回True
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
# 禁用requests的警告访问本地接口无需SSL验证避免控制台刷屏
requests.packages.urllib3.disable_warnings()
def check_appium_server_status(port=4723, timeout=30):
"""
检测指定端口的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验证
)
# 接口返回200HTTP成功状态码且JSON中status=0Appium服务正常
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.GLOBAL_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
try:
logging.info(f"{device_str}尝试关闭驱动 (尝试)")
driver.quit()
logging.info(f"{device_str}驱动已成功关闭")
return
except InvalidSessionIdException:
# 会话已经失效,不需要重试
logging.info(f"{device_str}会话已经失效,无需关闭")
return
except Exception as e:
logging.error(f"{device_str}关闭驱动时出错 : {str(e)}")
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 InvalidSessionIdException:
# # 会话已经失效,不需要重试
# 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 check_app_status(driver, package_name="com.bjjw.cjgc", activity=".activity.LoginActivity"):
"""
检查应用状态(不跳转页面)
参数:
driver: WebDriver实例
package_name: 应用包名,默认为"com.bjjw.cjgc"
activity: 应用启动Activity默认为".activity.LoginActivity"
返回:
bool: 应用是否正在运行
"""
try:
device_id = None
if not device_id:
device_id = global_variable.GLOBAL_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}")
# 检查应用是否在运行
if device_id:
# 使用ADB命令检查应用进程
cmd = [
"adb", "-s", device_id,
"shell", "ps", "|", "grep", package_name
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if package_name in result.stdout:
logging.info(f"{device_str}应用正在运行中")
return True
else:
logging.info(f"{device_str}应用未运行")
return False
else:
# 尝试使用Appium API检查应用状态
try:
if driver:
# 获取当前包名
current_package = driver.current_package
if current_package == package_name:
logging.info(f"{device_str}应用正在运行中(当前包名: {current_package}")
return True
else:
logging.info(f"{device_str}应用未运行(当前包名: {current_package}")
return False
except:
logging.warning(f"{device_str}无法获取当前应用状态")
return False
except Exception as e:
logging.error(f"检查应用状态时出错: {str(e)}")
logging.error(f"错误堆栈: {traceback.format_exc()}")
return False
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, package_name="com.bjjw.cjgc", activity=".activity.LoginActivity"):
"""
手动启动应用
参数:
driver: WebDriver实例
package_name: 应用包名,默认为"com.bjjw.cjgc"
activity: 应用启动Activity默认为".activity.LoginActivity"
"""
try:
device_id = global_variable.GLOBAL_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}")
# 首先尝试使用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命令启动应用")
# 等待应用启动
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):
# """
# 跳转到主页面并点击对应的导航菜单按钮
# 参数:
# driver: WebDriver实例
# device_id: 设备ID
# tabber_button_text: 导航菜单按钮的文本
# 返回:
# bool: 成功返回True失败返回False
# """
# try:
# # 检查当前是否已经在主页面
# 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:
# # 点击返回按钮
# # driver.press_keycode(4) # 4 是返回按钮的 keycode
# 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.error(f"设备 {device_id} 无法回到主页面当前Activity: {current_activity}")
# return False
# try:
# tabber_button = driver.find_element(AppiumBy.ID, tabber_button_text)
# # 点击按钮
# tabber_button.click()
# logging.info(f"设备 {device_id} 已成功点击导航菜单按钮: {tabber_button_text}")
# # 等待页面加载
# time.sleep(2)
# return True
# except TimeoutException:
# logging.error(f"设备 {device_id} 等待导航菜单按钮 '{tabber_button_text}' 超时")
# return False
# except Exception as e:
# logging.error(f"设备 {device_id} 点击导航菜单按钮 '{tabber_button_text}' 时出错: {str(e)}")
# return False
# except Exception as e:
# logging.error(f"设备 {device_id} 跳转到主页面并点击菜单按钮时出错: {str(e)}")
# return False
# 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} 次尝试执行导航操作")
# # 确保Appium服务器正在运行
# if not ensure_appium_server_running():
# logging.error(f"设备 {device_id} Appium服务器未运行启动失败")
# retry_count += 1
# if retry_count < max_retries:
# logging.info(f"设备 {device_id} 等待2秒后重试...")
# time.sleep(2)
# continue
# else:
# logging.error(f"设备 {device_id} 达到最大重试次数Appium服务器启动失败")
# return False
# # 检查会话有效性
# 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} 驱动重连失败")
# 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
# # 检查当前是否已经在主页面
# 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()
# 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 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()
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}'
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
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验证
)
# 接口返回200HTTP成功状态码且JSON中status=0Appium服务正常
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