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") 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秒 # # 添加额外的能力,避免初始化错误 # 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验证 ) # 接口返回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.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 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验证 ) # 接口返回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