From defefe1174b8d0ff291ec0a73b07e1f76b784a95 Mon Sep 17 00:00:00 2001 From: yupfa Date: Fri, 11 Apr 2025 23:17:00 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=9B=BF=E6=8D=A2tidevice=E4=B8=BAgo-ios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uiviewer/_device.py | 7 +- uiviewer/go_ios_cli.py | 644 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 648 insertions(+), 3 deletions(-) create mode 100644 uiviewer/go_ios_cli.py diff --git a/uiviewer/_device.py b/uiviewer/_device.py index ef4737b..d02d269 100644 --- a/uiviewer/_device.py +++ b/uiviewer/_device.py @@ -9,13 +9,13 @@ from PIL import Image from requests import request -import tidevice import adbutils import wda import uiautomator2 as u2 from hmdriver2 import hdc from fastapi import HTTPException +from uiviewer.go_ios_cli import GoIOS from uiviewer._logger import logger from uiviewer._utils import file2base64, image2base64 from uiviewer._models import Platform, BaseHierarchy @@ -28,8 +28,9 @@ def list_serials(platform: str) -> List[str]: raws = adbutils.AdbClient().device_list() devices = [item.serial for item in raws] elif platform == Platform.IOS: - raw = tidevice.Usbmux().device_list() - devices = [d.udid for d in raw] + raw = GoIOS.list_devices() + + devices = raw.get("deviceList", []) else: devices = hdc.list_devices() diff --git a/uiviewer/go_ios_cli.py b/uiviewer/go_ios_cli.py new file mode 100644 index 0000000..fbba088 --- /dev/null +++ b/uiviewer/go_ios_cli.py @@ -0,0 +1,644 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +go-ios命令封装模块 +提供对go-ios命令的封装,方便在Python中使用 +""" + +import os +import json +import time +import subprocess +from typing import Dict, List, Optional, Union, Any + +# 配置日志 + + +class GoIOSError(Exception): + """go-ios命令执行错误""" + def __init__(self, message: str, cmd: str = "", stderr: str = ""): + self.message = message + self.cmd = cmd + self.stderr = stderr + super().__init__(f"{message}, cmd: {cmd}, stderr: {stderr}") + + +class SimulatorError(Exception): + """模拟器操作异常类""" + + def __init__(self, message: str, cmd: str = "", stderr: str = ""): + self.message = message + self.cmd = cmd + self.stderr = stderr + super().__init__(self.message) + + def __str__(self): + return f"{self.message} (cmd: {self.cmd}, stderr: {self.stderr})" + + +class GoIOS: + """go-ios命令封装类""" + + @staticmethod + def _run_command(cmd: List[str], check: bool = True, json_output: bool = True) -> Union[Dict, str]: + """ + 运行go-ios命令 + + Args: + cmd: 命令列表 + check: 是否检查命令执行状态 + json_output: 是否将输出解析为JSON + + Returns: + Dict或str: 命令执行结果 + + Raises: + GoIOSError: 命令执行错误 + """ + try: + # logger.debug(f"执行命令: {' '.join(cmd)}") + result = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=check + ) + + if result.returncode != 0: + raise GoIOSError( + f"命令执行失败,返回码: {result.returncode}", + cmd=' '.join(cmd), + stderr=result.stderr + ) + + if json_output: + try: + return json.loads(result.stdout) + except json.JSONDecodeError: + # logger.warning(f"无法解析JSON输出: {result.stdout}") + return result.stdout + else: + return result.stdout + except subprocess.CalledProcessError as e: + raise GoIOSError( + f"命令执行异常: {e}", + cmd=' '.join(cmd), + stderr=e.stderr if hasattr(e, 'stderr') else "" + ) + except Exception as e: + raise GoIOSError(f"执行命令时发生错误: {e}", cmd=' '.join(cmd)) + + @classmethod + def get_version(cls) -> Dict: + """ + 获取go-ios版本信息 + + Returns: + Dict: 版本信息 + """ + return cls._run_command(["ios", "version"]) + + @classmethod + def list_devices(cls, details: bool = False) -> Dict: + """ + 获取设备列表 + + Args: + details: 是否获取详细信息 + + Returns: + Dict: 设备列表 + """ + cmd = ["ios", "list"] + if details: + cmd.append("--details") + return cls._run_command(cmd) + + @classmethod + def get_device_info(cls, udid: str) -> Dict: + """ + 获取设备信息 + + Args: + udid: 设备UDID + + Returns: + Dict: 设备信息 + """ + return cls._run_command(["ios", "info", f"--udid={udid}"]) + + @classmethod + def ios_tunnel_status(cls,udid:str=None) -> Dict: + """ + 启动隧道 + + Returns: + Dict: 隧道信息 + """ + cmd = ["ios", "tunnel", "ls"] + if udid: + cmd.append(f"--udid={udid}") + return cls._run_command(cmd) + + @classmethod + def forward_port(cls, host_port: int, device_port: int, udid: Optional[str] = None) -> Dict: + """ + 端口转发 + + Args: + host_port: 主机端口 + device_port: 设备端口 + udid: 设备UDID,如果为None则使用第一个可用设备 + + Returns: + Dict: 转发结果 + """ + cmd = ["ios", "forward", f"{host_port}", f"{device_port}"] + if udid: + cmd.append(f"--udid={udid}") + return cls._run_command(cmd) + + @classmethod + def remove_forward(cls, host_port: int, udid: Optional[str] = None) -> Dict: + """ + 移除端口转发 + + Args: + host_port: 主机端口 + udid: 设备UDID,如果为None则使用第一个可用设备 + + Returns: + Dict: 移除结果 + """ + cmd = ["ios", "forward", "--remove", f"{host_port}"] + if udid: + cmd.append(f"--udid={udid}") + return cls._run_command(cmd) + + @classmethod + def list_forward(cls, udid: Optional[str] = None) -> Dict: + """ + 列出端口转发 + + Args: + udid: 设备UDID,如果为None则使用第一个可用设备 + + Returns: + Dict: 转发列表 + """ + cmd = ["ios", "forward", "--list"] + if udid: + cmd.append(f"--udid={udid}") + return cls._run_command(cmd) + + @classmethod + def run_wda(cls, bundle_id: str, test_runner_bundle_id: str, + xctestconfig: str = "WebDriverAgentRunner.xctest", + udid: Optional[str] = None) -> subprocess.Popen: + """ + 运行WDA(非阻塞方式) + + Args: + bundle_id: WDA Bundle ID + test_runner_bundle_id: Test Runner Bundle ID + xctestconfig: XCTest配置 + udid: 设备UDID,如果为None则使用第一个可用设备 + + Returns: + subprocess.Popen: 进程对象,可用于后续管理 + """ + cmd = [ + "ios", "runwda", + f"--bundleid={bundle_id}", + f"--testrunnerbundleid={test_runner_bundle_id}", + f"--xctestconfig={xctestconfig}" + ] + if udid: + cmd.append(f"--udid={udid}") + + # logger.debug(f"执行命令: {' '.join(cmd)}") + try: + # 使用Popen非阻塞方式启动WDA + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + # 等待一小段时间,确保进程启动 + time.sleep(1) + + # 检查进程是否已经终止(表示启动失败) + if process.poll() is not None: + stderr = process.stderr.read() if process.stderr else "" + raise GoIOSError( + f"WDA启动失败,进程已终止,返回码: {process.returncode}", + cmd=' '.join(cmd), + stderr=stderr + ) + + # logger.info("WDA启动成功(非阻塞)") + return process + except Exception as e: + if not isinstance(e, GoIOSError): + e = GoIOSError(f"启动WDA时发生错误: {e}", cmd=' '.join(cmd)) + raise e + + @classmethod + def list_apps(cls, udid:str, is_system:bool=False, is_all:bool=False) -> Dict: + """ + 获取应用列表 + + Args: + udid: 设备UDID,如果为None则使用第一个可用设备 + + Returns: + Dict: 应用列表 + """ + cmd = ["ios", "apps", '--list'] + if is_system: + cmd.append("--system") + if is_all: + cmd.append("--all") + cmd.append(f"--udid={udid}") + + return cls._run_command(cmd) + + @classmethod + def install_app(cls, ipa_path: str, udid: Optional[str] = None) -> Dict: + """ + 安装应用 + + Args: + ipa_path: IPA文件路径 + udid: 设备UDID,如果为None则使用第一个可用设备 + + Returns: + Dict: 安装结果 + """ + cmd = ["ios", "install", ipa_path] + if udid: + cmd.append(f"--udid={udid}") + return cls._run_command(cmd) + + @classmethod + def uninstall_app(cls, bundle_id: str, udid: Optional[str] = None) -> Dict: + """ + 卸载应用 + + Args: + bundle_id: 应用Bundle ID + udid: 设备UDID,如果为None则使用第一个可用设备 + + Returns: + Dict: 卸载结果 + """ + cmd = ["ios", "uninstall", bundle_id] + if udid: + cmd.append(f"--udid={udid}") + return cls._run_command(cmd) + + @classmethod + def launch_app(cls, bundle_id: str, udid: Optional[str] = None) -> Dict: + """ + 启动应用 + + Args: + bundle_id: 应用Bundle ID + udid: 设备UDID,如果为None则使用第一个可用设备 + + Returns: + Dict: 启动结果 + """ + cmd = ["ios", "launch", bundle_id] + if udid: + cmd.append(f"--udid={udid}") + return cls._run_command(cmd) + + @classmethod + def terminate_app(cls, bundle_id: str, udid: Optional[str] = None) -> Dict: + """ + 终止应用 + + Args: + bundle_id: 应用Bundle ID + udid: 设备UDID,如果为None则使用第一个可用设备 + + Returns: + Dict: 终止结果 + """ + cmd = ["ios", "kill", bundle_id] + if udid: + cmd.append(f"--udid={udid}") + return cls._run_command(cmd) + + @classmethod + def get_app_state(cls, bundle_id: str, udid: Optional[str] = None) -> Dict: + """ + 获取应用状态 + + Args: + bundle_id: 应用Bundle ID + udid: 设备UDID,如果为None则使用第一个可用设备 + + Returns: + Dict: 应用状态 + """ + cmd = ["ios", "appstate", bundle_id] + if udid: + cmd.append(f"--udid={udid}") + return cls._run_command(cmd) + + @classmethod + def take_screenshot(cls, output_path: Optional[str], udid: Optional[str] = None) -> str: + """ + 截图 + + Args: + output_path: 输出路径,如果为None则使用临时文件 + udid: 设备UDID,如果为None则使用第一个可用设备 + + Returns: + str: 截图路径 + """ + if not output_path: + raise ValueError("输出路径不能为空") + + cmd = ["ios", "screenshot", f"--output={output_path}"] + if udid: + cmd.append(f"--udid={udid}") + + try: + cls._run_command(cmd, json_output=False) + return output_path + except Exception as e: + # logger.error(f"截图失败: {e}") + return "" + + +class Simulator: + """iOS模拟器操作类,基于xcrun simctl命令""" + + @classmethod + def _run_command(cls, cmd: List[str], json_output: bool = True, check: bool = True) -> Union[Dict, str]: + """ + 运行命令并返回结果 + + Args: + cmd: 命令 + json_output: 是否解析JSON输出 + check: 是否检查返回码 + + Returns: + Dict或str: 命令输出 + """ + # logger.debug(f"执行命令: {' '.join(cmd)}") + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=check) + if result.returncode != 0: + raise SimulatorError( + f"命令执行失败,返回码: {result.returncode}", + cmd=' '.join(cmd), + stderr=result.stderr + ) + + if json_output: + try: + return json.loads(result.stdout) + except json.JSONDecodeError: + raise SimulatorError( + f"解析JSON输出失败", + cmd=' '.join(cmd), + stderr=result.stderr + ) + else: + return result.stdout.strip() + except subprocess.CalledProcessError as e: + raise SimulatorError( + f"命令执行失败: {e}", + cmd=' '.join(cmd), + stderr=e.stderr if hasattr(e, 'stderr') else "" + ) + except Exception as e: + if not isinstance(e, SimulatorError): + raise SimulatorError(f"命令执行出错: {e}", cmd=' '.join(cmd)) + raise e + + @classmethod + def list_devices(cls) -> Dict: + """ + 列出所有可用的模拟器 + + Returns: + Dict: 模拟器列表 + """ + cmd = ["xcrun", "simctl", "list", "devices", "--json"] + return cls._run_command(cmd) + + @classmethod + def boot_device(cls, udid: str) -> str: + """ + 启动指定的模拟器 + + Args: + udid: 模拟器UDID + + Returns: + str: 启动结果 + """ + cmd = ["xcrun", "simctl", "boot", udid] + return cls._run_command(cmd, json_output=False) + + @classmethod + def shutdown_device(cls, udid: str) -> str: + """ + 关闭指定的模拟器 + + Args: + udid: 模拟器UDID + + Returns: + str: 关闭结果 + """ + cmd = ["xcrun", "simctl", "shutdown", udid] + return cls._run_command(cmd, json_output=False) + + @classmethod + def erase_device(cls, udid: str) -> str: + """ + 擦除指定模拟器的所有内容 + + Args: + udid: 模拟器UDID + + Returns: + str: 擦除结果 + """ + cmd = ["xcrun", "simctl", "erase", udid] + return cls._run_command(cmd, json_output=False) + + @classmethod + def create_device(cls, name: str, device_type: str, runtime: str) -> str: + """ + 创建新的模拟器 + + Args: + name: 模拟器名称 + device_type: 设备类型,例如 "iPhone 12" + runtime: 运行时版本,例如 "iOS-14-0" + + Returns: + str: 创建结果,包含新模拟器的UDID + """ + cmd = ["xcrun", "simctl", "create", name, device_type, runtime] + return cls._run_command(cmd, json_output=False) + + @classmethod + def delete_device(cls, udid: str) -> str: + """ + 删除指定的模拟器 + + Args: + udid: 模拟器UDID + + Returns: + str: 删除结果 + """ + cmd = ["xcrun", "simctl", "delete", udid] + return cls._run_command(cmd, json_output=False) + + @classmethod + def list_runtimes(cls) -> Dict: + """ + 列出所有可用的模拟器运行时 + + Returns: + Dict: 运行时列表 + """ + cmd = ["xcrun", "simctl", "list", "runtimes", "--json"] + return cls._run_command(cmd) + + @classmethod + def list_device_types(cls) -> Dict: + """ + 列出所有可用的模拟器设备类型 + + Returns: + Dict: 设备类型列表 + """ + cmd = ["xcrun", "simctl", "list", "devicetypes", "--json"] + return cls._run_command(cmd) + + @classmethod + def install_app(cls, udid: str, app_path: str) -> str: + """ + 在模拟器上安装应用 + + Args: + udid: 模拟器UDID + app_path: 应用路径 + + Returns: + str: 安装结果 + """ + cmd = ["xcrun", "simctl", "install", udid, app_path] + return cls._run_command(cmd, json_output=False) + + @classmethod + def uninstall_app(cls, udid: str, bundle_id: str) -> str: + """ + 从模拟器卸载应用 + + Args: + udid: 模拟器UDID + bundle_id: 应用Bundle ID + + Returns: + str: 卸载结果 + """ + cmd = ["xcrun", "simctl", "uninstall", udid, bundle_id] + return cls._run_command(cmd, json_output=False) + + @classmethod + def launch_app(cls, udid: str, bundle_id: str) -> str: + """ + 在模拟器上启动应用 + + Args: + udid: 模拟器UDID + bundle_id: 应用Bundle ID + + Returns: + str: 启动结果 + """ + cmd = ["xcrun", "simctl", "launch", udid, bundle_id] + return cls._run_command(cmd, json_output=False) + + @classmethod + def terminate_app(cls, udid: str, bundle_id: str) -> str: + """ + 终止模拟器上的应用 + + Args: + udid: 模拟器UDID + bundle_id: 应用Bundle ID + + Returns: + str: 终止结果 + """ + cmd = ["xcrun", "simctl", "terminate", udid, bundle_id] + return cls._run_command(cmd, json_output=False) + + @classmethod + def take_screenshot(cls, udid: str, output_path: str) -> str: + """ + 模拟器截图 + + Args: + udid: 模拟器UDID + output_path: 输出路径 + + Returns: + str: 截图结果 + """ + cmd = ["xcrun", "simctl", "io", udid, "screenshot", output_path] + return cls._run_command(cmd, json_output=False) + + @classmethod + def open_url(cls, udid: str, url: str) -> str: + """ + 在模拟器上打开URL + + Args: + udid: 模拟器UDID + url: URL + + Returns: + str: 打开结果 + """ + cmd = ["xcrun", "simctl", "openurl", udid, url] + return cls._run_command(cmd, json_output=False) + + @classmethod + def get_app_container(cls, udid: str, bundle_id: str, container_type: str = "app") -> str: + """ + 获取应用容器路径 + + Args: + udid: 模拟器UDID + bundle_id: 应用Bundle ID + container_type: 容器类型,可选值: app, data, groups + + Returns: + str: 容器路径 + """ + cmd = ["xcrun", "simctl", "get_app_container", udid, bundle_id, container_type] + return cls._run_command(cmd, json_output=False) + + +if __name__ == "__main__": + # # 测试代码 + devices = GoIOS.list_devices() + print(devices) \ No newline at end of file From c4c16098aa28b0b5f316ca5dac454a1ca75bcbdb Mon Sep 17 00:00:00 2001 From: yupfa Date: Mon, 14 Apr 2025 14:27:49 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E4=BC=98=E5=8C=96go-ios=20=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E8=A1=8C=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- uiviewer/go_ios_cli.py | 273 ----------------------------------------- 2 files changed, 1 insertion(+), 274 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b39ced4..00df85a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ fastapi = "^0.68.0" aiofiles = "^23.1.0" uiautomator2 = "^3.0.0" facebook-wda = "^1.0.5" -tidevice = "^0.12.10" +#tidevice = "^0.12.10" hmdriver2 = "^1.4.0" [tool.poetry.extras] diff --git a/uiviewer/go_ios_cli.py b/uiviewer/go_ios_cli.py index fbba088..c9e29c2 100644 --- a/uiviewer/go_ios_cli.py +++ b/uiviewer/go_ios_cli.py @@ -24,19 +24,6 @@ def __init__(self, message: str, cmd: str = "", stderr: str = ""): super().__init__(f"{message}, cmd: {cmd}, stderr: {stderr}") -class SimulatorError(Exception): - """模拟器操作异常类""" - - def __init__(self, message: str, cmd: str = "", stderr: str = ""): - self.message = message - self.cmd = cmd - self.stderr = stderr - super().__init__(self.message) - - def __str__(self): - return f"{self.message} (cmd: {self.cmd}, stderr: {self.stderr})" - - class GoIOS: """go-ios命令封装类""" @@ -378,266 +365,6 @@ def take_screenshot(cls, output_path: Optional[str], udid: Optional[str] = None) return "" -class Simulator: - """iOS模拟器操作类,基于xcrun simctl命令""" - - @classmethod - def _run_command(cls, cmd: List[str], json_output: bool = True, check: bool = True) -> Union[Dict, str]: - """ - 运行命令并返回结果 - - Args: - cmd: 命令 - json_output: 是否解析JSON输出 - check: 是否检查返回码 - - Returns: - Dict或str: 命令输出 - """ - # logger.debug(f"执行命令: {' '.join(cmd)}") - try: - result = subprocess.run(cmd, capture_output=True, text=True, check=check) - if result.returncode != 0: - raise SimulatorError( - f"命令执行失败,返回码: {result.returncode}", - cmd=' '.join(cmd), - stderr=result.stderr - ) - - if json_output: - try: - return json.loads(result.stdout) - except json.JSONDecodeError: - raise SimulatorError( - f"解析JSON输出失败", - cmd=' '.join(cmd), - stderr=result.stderr - ) - else: - return result.stdout.strip() - except subprocess.CalledProcessError as e: - raise SimulatorError( - f"命令执行失败: {e}", - cmd=' '.join(cmd), - stderr=e.stderr if hasattr(e, 'stderr') else "" - ) - except Exception as e: - if not isinstance(e, SimulatorError): - raise SimulatorError(f"命令执行出错: {e}", cmd=' '.join(cmd)) - raise e - - @classmethod - def list_devices(cls) -> Dict: - """ - 列出所有可用的模拟器 - - Returns: - Dict: 模拟器列表 - """ - cmd = ["xcrun", "simctl", "list", "devices", "--json"] - return cls._run_command(cmd) - - @classmethod - def boot_device(cls, udid: str) -> str: - """ - 启动指定的模拟器 - - Args: - udid: 模拟器UDID - - Returns: - str: 启动结果 - """ - cmd = ["xcrun", "simctl", "boot", udid] - return cls._run_command(cmd, json_output=False) - - @classmethod - def shutdown_device(cls, udid: str) -> str: - """ - 关闭指定的模拟器 - - Args: - udid: 模拟器UDID - - Returns: - str: 关闭结果 - """ - cmd = ["xcrun", "simctl", "shutdown", udid] - return cls._run_command(cmd, json_output=False) - - @classmethod - def erase_device(cls, udid: str) -> str: - """ - 擦除指定模拟器的所有内容 - - Args: - udid: 模拟器UDID - - Returns: - str: 擦除结果 - """ - cmd = ["xcrun", "simctl", "erase", udid] - return cls._run_command(cmd, json_output=False) - - @classmethod - def create_device(cls, name: str, device_type: str, runtime: str) -> str: - """ - 创建新的模拟器 - - Args: - name: 模拟器名称 - device_type: 设备类型,例如 "iPhone 12" - runtime: 运行时版本,例如 "iOS-14-0" - - Returns: - str: 创建结果,包含新模拟器的UDID - """ - cmd = ["xcrun", "simctl", "create", name, device_type, runtime] - return cls._run_command(cmd, json_output=False) - - @classmethod - def delete_device(cls, udid: str) -> str: - """ - 删除指定的模拟器 - - Args: - udid: 模拟器UDID - - Returns: - str: 删除结果 - """ - cmd = ["xcrun", "simctl", "delete", udid] - return cls._run_command(cmd, json_output=False) - - @classmethod - def list_runtimes(cls) -> Dict: - """ - 列出所有可用的模拟器运行时 - - Returns: - Dict: 运行时列表 - """ - cmd = ["xcrun", "simctl", "list", "runtimes", "--json"] - return cls._run_command(cmd) - - @classmethod - def list_device_types(cls) -> Dict: - """ - 列出所有可用的模拟器设备类型 - - Returns: - Dict: 设备类型列表 - """ - cmd = ["xcrun", "simctl", "list", "devicetypes", "--json"] - return cls._run_command(cmd) - - @classmethod - def install_app(cls, udid: str, app_path: str) -> str: - """ - 在模拟器上安装应用 - - Args: - udid: 模拟器UDID - app_path: 应用路径 - - Returns: - str: 安装结果 - """ - cmd = ["xcrun", "simctl", "install", udid, app_path] - return cls._run_command(cmd, json_output=False) - - @classmethod - def uninstall_app(cls, udid: str, bundle_id: str) -> str: - """ - 从模拟器卸载应用 - - Args: - udid: 模拟器UDID - bundle_id: 应用Bundle ID - - Returns: - str: 卸载结果 - """ - cmd = ["xcrun", "simctl", "uninstall", udid, bundle_id] - return cls._run_command(cmd, json_output=False) - - @classmethod - def launch_app(cls, udid: str, bundle_id: str) -> str: - """ - 在模拟器上启动应用 - - Args: - udid: 模拟器UDID - bundle_id: 应用Bundle ID - - Returns: - str: 启动结果 - """ - cmd = ["xcrun", "simctl", "launch", udid, bundle_id] - return cls._run_command(cmd, json_output=False) - - @classmethod - def terminate_app(cls, udid: str, bundle_id: str) -> str: - """ - 终止模拟器上的应用 - - Args: - udid: 模拟器UDID - bundle_id: 应用Bundle ID - - Returns: - str: 终止结果 - """ - cmd = ["xcrun", "simctl", "terminate", udid, bundle_id] - return cls._run_command(cmd, json_output=False) - - @classmethod - def take_screenshot(cls, udid: str, output_path: str) -> str: - """ - 模拟器截图 - - Args: - udid: 模拟器UDID - output_path: 输出路径 - - Returns: - str: 截图结果 - """ - cmd = ["xcrun", "simctl", "io", udid, "screenshot", output_path] - return cls._run_command(cmd, json_output=False) - - @classmethod - def open_url(cls, udid: str, url: str) -> str: - """ - 在模拟器上打开URL - - Args: - udid: 模拟器UDID - url: URL - - Returns: - str: 打开结果 - """ - cmd = ["xcrun", "simctl", "openurl", udid, url] - return cls._run_command(cmd, json_output=False) - - @classmethod - def get_app_container(cls, udid: str, bundle_id: str, container_type: str = "app") -> str: - """ - 获取应用容器路径 - - Args: - udid: 模拟器UDID - bundle_id: 应用Bundle ID - container_type: 容器类型,可选值: app, data, groups - - Returns: - str: 容器路径 - """ - cmd = ["xcrun", "simctl", "get_app_container", udid, bundle_id, container_type] - return cls._run_command(cmd, json_output=False) - - if __name__ == "__main__": # # 测试代码 devices = GoIOS.list_devices() From cd30ba543c9237e137a03ead9462e15ed3859354 Mon Sep 17 00:00:00 2001 From: yupfa Date: Mon, 14 Apr 2025 15:16:11 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=90=AF=E5=8A=A8wda=20?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9B=E5=90=AF=E5=8A=A8=E6=88=90=E5=8A=9F?= =?UTF-8?q?=E5=90=8E=EF=BC=8C=E8=87=AA=E5=8A=A8=E5=9B=9E=E5=86=99wda=20url?= =?UTF-8?q?=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uiviewer/routers/api.py | 31 +++++++++++-- uiviewer/static/index.html | 31 +++++++++++++ uiviewer/static/js/api.js | 12 +++++ uiviewer/static/js/index.js | 93 ++++++++++++++++++++++++++++++++++--- 4 files changed, 158 insertions(+), 9 deletions(-) diff --git a/uiviewer/routers/api.py b/uiviewer/routers/api.py index 8b40318..8d74bb7 100644 --- a/uiviewer/routers/api.py +++ b/uiviewer/routers/api.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- -from typing import Union, Dict, Any +from typing import Union, Dict, Any, Optional -from fastapi import APIRouter, Query +from fastapi import APIRouter, Query, HTTPException from fastapi.responses import RedirectResponse +from pydantic import BaseModel from uiviewer._device import ( list_serials, @@ -16,11 +17,19 @@ from uiviewer._version import __version__ from uiviewer._models import ApiResponse, XPathLiteRequest from uiviewer.parser.xpath_lite import XPathLiteGenerator +from uiviewer.go_ios_cli import GoIOS, GoIOSError router = APIRouter() +class RunWdaRequest(BaseModel): + bundle_id: str + test_runner_bundle_id: str + xctestconfig: str = "WebDriverAgentRunner.xctest" + udid: Optional[str] = None + + @router.get("/") def root(): return RedirectResponse(url="/static/index.html") @@ -73,4 +82,20 @@ async def fetch_xpathLite(platform: str, request: XPathLiteRequest): node_id = request.node_id generator = XPathLiteGenerator(platform, tree_data) xpath = generator.get_xpathLite(node_id) - return ApiResponse.doSuccess(xpath) \ No newline at end of file + return ApiResponse.doSuccess(xpath) + + +@router.post("/ios/run_wda", response_model=ApiResponse) +async def run_wda(request: RunWdaRequest): + try: + GoIOS.run_wda( + bundle_id=request.bundle_id, + test_runner_bundle_id=request.test_runner_bundle_id, + xctestconfig=request.xctestconfig, + udid=request.udid + ) + return ApiResponse.doSuccess("WDA start command issued successfully.") + except GoIOSError as e: + return ApiResponse.doError(f"Failed to start WDA: {e.message}. Details: {e.stderr}") + except Exception as e: + return ApiResponse.doError(f"An unexpected error occurred: {str(e)}") \ No newline at end of file diff --git a/uiviewer/static/index.html b/uiviewer/static/index.html index d250d86..6739037 100644 --- a/uiviewer/static/index.html +++ b/uiviewer/static/index.html @@ -62,6 +62,11 @@ + 启动 WDA + + + + + + + + + + + + + + + + + + + 取 消 + 启 动 + + + diff --git a/uiviewer/static/js/api.js b/uiviewer/static/js/api.js index 62b2193..50ac57a 100644 --- a/uiviewer/static/js/api.js +++ b/uiviewer/static/js/api.js @@ -63,5 +63,17 @@ export async function fetchXpathLite(platform, treeData, nodeId) { }) }); + return checkResponse(response); +} + +// Added function to call the backend endpoint for starting WDA +export async function startWdaProcess(payload) { + const response = await fetch(`${API_HOST}ios/run_wda`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) // Send bundle_id, test_runner_bundle_id, xctestconfig, udid + }); return checkResponse(response); } \ No newline at end of file diff --git a/uiviewer/static/js/index.js b/uiviewer/static/js/index.js index 216307a..5e34730 100644 --- a/uiviewer/static/js/index.js +++ b/uiviewer/static/js/index.js @@ -1,5 +1,13 @@ import { saveToLocalStorage, getFromLocalStorage, copyToClipboard } from './utils.js'; -import { getVersion, listDevices, connectDevice, fetchScreenshot, fetchHierarchy, fetchXpathLite } from './api.js'; +import { + getVersion, + listDevices, + connectDevice, + fetchScreenshot, + fetchHierarchy, + fetchXpathLite, + startWdaProcess +} from './api.js'; new Vue({ @@ -35,7 +43,16 @@ new Vue({ nodeFilterText: '', centerWidth: 500, isDividerHovered: false, - isDragging: false + isDragging: false, + + wdaDialogVisible: false, + wdaStarting: false, + wdaForm: { + bundle_id: getFromLocalStorage('wda_bundle_id', ''), + test_runner_bundle_id: getFromLocalStorage('wda_test_runner_bundle_id', ''), + xctestconfig: getFromLocalStorage('wda_xctestconfig', 'WebDriverAgentRunner.xctest'), + udid: '' + } }; }, computed: { @@ -69,6 +86,14 @@ new Vue({ }, nodeFilterText(val) { this.$refs.treeRef.filter(val); + }, + wdaForm: { + deep: true, + handler(newVal) { + saveToLocalStorage('wda_bundle_id', newVal.bundle_id); + saveToLocalStorage('wda_test_runner_bundle_id', newVal.test_runner_bundle_id); + saveToLocalStorage('wda_xctestconfig', newVal.xctestconfig); + } } }, created() { @@ -81,7 +106,6 @@ new Vue({ canvas.addEventListener('click', this.onMouseClick); canvas.addEventListener('mouseleave', this.onMouseLeave); - // 设置Canvas的尺寸和分辨率 this.setupCanvasResolution('#screenshotCanvas'); this.setupCanvasResolution('#hierarchyCanvas'); }, @@ -242,7 +266,6 @@ new Vue({ } }, - // 解决在高分辨率屏幕上,Canvas绘制的内容可能会显得模糊。这是因为Canvas的默认分辨率与屏幕的物理像素密度不匹配 setupCanvasResolution(selector) { const canvas = this.$el.querySelector(selector); const dpr = window.devicePixelRatio || 1; @@ -367,7 +390,6 @@ new Vue({ this.renderHierarchy(); } else { - // 保证每次点击重新计算`selectedNodeDetails`,更新点击坐标 this.selectedNode = { ...this.selectedNode }; } }, @@ -430,7 +452,66 @@ new Vue({ this.isDividerHovered = true; }, leaveDivider() { - this.isDividerHovered = false; + if (!this.isDragging) { + this.isDividerHovered = false; + } + }, + showWdaDialog() { + this.wdaForm.udid = this.serial || ''; + this.wdaDialogVisible = true; + this.$nextTick(() => { + if (this.$refs.wdaFormRef) { + this.$refs.wdaFormRef.clearValidate(); + } + }); + }, + + async startWda() { + if (this.platform !== 'ios') { + this.$message.error('WDA can only be started for iOS platform.'); + return; + } + + this.$refs.wdaFormRef.validate(async (valid) => { + if (valid) { + this.wdaStarting = true; + try { + const udidToUse = this.wdaForm.udid || this.serial; + if (!udidToUse && !this.wdaForm.udid) { + console.warn("No UDID selected or entered for WDA."); + } + + const payload = { + bundle_id: this.wdaForm.bundle_id, + test_runner_bundle_id: this.wdaForm.test_runner_bundle_id, + xctestconfig: this.wdaForm.xctestconfig || 'WebDriverAgentRunner.xctest', + udid: udidToUse || null + }; + + const response = await startWdaProcess(payload); + if (response.success) { + this.$message.success('WDA start command issued successfully.'); + this.wdaDialogVisible = false; + this.wdaUrl= "http://localhost:8100"; + } else { + throw new Error(response.message || 'Failed to issue WDA start command.'); + } + } catch (err) { + console.error('Error starting WDA:', err); + this.$message({ + showClose: true, + message: `启动 WDA 失败: ${err.message}`, + type: 'error', + duration: 5000 + }); + } finally { + this.wdaStarting = false; + } + } else { + console.log('WDA form validation failed'); + return false; + } + }); } } }); \ No newline at end of file