From e6323bf229abfd703e674bdf49adb83fbb65e547 Mon Sep 17 00:00:00 2001 From: Mahmudul Alam Date: Sun, 30 Nov 2025 05:13:30 +0600 Subject: [PATCH 01/17] feat: added linux installation functionality --- Framework/install_handler/linux/atspi.py | 154 +++++++++++++++++ .../install_handler/linux/linux_utils.py | 160 ++++++++++++++++++ Framework/install_handler/linux/xwd.py | 142 ++++++++++++++++ Framework/install_handler/route.py | 79 ++++++--- 4 files changed, 510 insertions(+), 25 deletions(-) create mode 100644 Framework/install_handler/linux/atspi.py create mode 100644 Framework/install_handler/linux/linux_utils.py create mode 100644 Framework/install_handler/linux/xwd.py diff --git a/Framework/install_handler/linux/atspi.py b/Framework/install_handler/linux/atspi.py new file mode 100644 index 000000000..39130f126 --- /dev/null +++ b/Framework/install_handler/linux/atspi.py @@ -0,0 +1,154 @@ +from Framework.install_handler.utils import send_response +from .linux_utils import ( + detect_package_manager, + check_all_packages_installed, + install_packages, +) + + +# Package definitions for different package managers +PACKAGES = { + "apt": [ + "build-essential", + "cmake", + "pkg-config", + "libgirepository1.0-dev", + "libcairo2-dev", + "xdotool", + ], + "dnf": [ + "cmake", + "pkgconf-pkg-config", + "gobject-introspection-devel", + "cairo-devel", + "python3-devel", + "cairo-gobject-devel", + "xdotool", + ], + "pacman": [ + "gcc", + "meson", + "cmake", + "pkgconf", + "cairo", + "xdotool", + "gobject-introspection", + ], +} + + +async def check_status(): + """Checks if AT-SPI development packages are installed.""" + + package_manager, _ = detect_package_manager() + + if not package_manager: + await send_response( + { + "action": "status", + "data": { + "category": "Linux Accessibility", + "name": "AT-SPI Packages", + "status": "error", + "comment": "Unsupported package manager. Only apt, dnf, and pacman are supported.", + }, + } + ) + return False + + packages = PACKAGES.get(package_manager, []) + if check_all_packages_installed(package_manager, packages): + await send_response( + { + "action": "status", + "data": { + "category": "Linux Accessibility", + "name": "AT-SPI Packages", + "status": "installed", + "comment": "AT-SPI development packages are installed.", + }, + } + ) + return True + else: + await send_response( + { + "action": "status", + "data": { + "category": "Linux Accessibility", + "name": "AT-SPI Packages", + "status": "not installed", + "comment": f"Install AT-SPI packages using {package_manager}.", + }, + } + ) + return False + + +async def install(user_password: str = ""): + """Install AT-SPI development packages using the system package manager.""" + + is_already_installed = await check_status() + + if not is_already_installed: + package_manager, _ = detect_package_manager() + + if not package_manager: + await send_response( + { + "action": "status", + "data": { + "category": "Linux Accessibility", + "name": "AT-SPI Packages", + "status": "error", + "comment": "Unsupported package manager. Only apt, dnf, and pacman are supported.", + }, + } + ) + return False + + packages = PACKAGES.get(package_manager, []) + await send_response( + { + "action": "status", + "data": { + "category": "Linux Accessibility", + "name": "AT-SPI Packages", + "status": "installing", + "comment": f"Installing packages using {package_manager}, please wait...", + }, + } + ) + + success, error_msg = install_packages( + package_manager, packages, user_password, timeout=3600 + ) + + if success: + await send_response( + { + "action": "status", + "data": { + "category": "Linux Accessibility", + "name": "AT-SPI Packages", + "status": "installed", + "comment": "AT-SPI packages have been installed successfully.", + }, + } + ) + return True + else: + await send_response( + { + "action": "status", + "data": { + "category": "Linux Accessibility", + "name": "AT-SPI Packages", + "status": "error", + "comment": f"Installation failed. Error: {error_msg}", + }, + } + ) + return False + else: + return True diff --git a/Framework/install_handler/linux/linux_utils.py b/Framework/install_handler/linux/linux_utils.py new file mode 100644 index 000000000..603e36a17 --- /dev/null +++ b/Framework/install_handler/linux/linux_utils.py @@ -0,0 +1,160 @@ +"""Shared utilities for Linux package management.""" + +import subprocess +import shutil + + +def detect_package_manager(): + """Detect the available package manager on the system. + + Returns: + tuple: (package_manager_name, None) where package_manager_name is one of "apt", "dnf", "pacman", or None + """ + if shutil.which("apt-get"): + return "apt", None + elif shutil.which("dnf"): + return "dnf", None + elif shutil.which("pacman"): + return "pacman", None + else: + return None, None + + +def check_package_installed(package_manager, package): + """Check if a package is installed using the appropriate package manager. + + Args: + package_manager: One of "apt", "dnf", or "pacman" + package: Package name to check + + Returns: + bool: True if package is installed, False otherwise + """ + try: + if package_manager == "apt": + result = subprocess.run( + ["dpkg", "-s", package], capture_output=True, text=True, timeout=30 + ) + return result.returncode == 0 + elif package_manager == "dnf": + result = subprocess.run( + ["rpm", "-q", package], capture_output=True, text=True, timeout=30 + ) + return result.returncode == 0 + elif package_manager == "pacman": + result = subprocess.run( + ["pacman", "-Q", package], capture_output=True, text=True, timeout=30 + ) + return result.returncode == 0 + except Exception: + pass + return False + + +def check_all_packages_installed(package_manager, packages): + """Check if all packages in the list are installed. + + Args: + package_manager: One of "apt", "dnf", or "pacman" + packages: List of package names to check + + Returns: + bool: True if all packages are installed, False otherwise + """ + for package in packages: + if not check_package_installed(package_manager, package): + return False + return True + + +def install_packages(package_manager, packages, user_password="", timeout=3600): + """Install packages using the system package manager. + + Args: + package_manager: One of "apt", "dnf", or "pacman" + packages: List of package names to install + user_password: Optional sudo password for authentication + timeout: Timeout in seconds for the installation (default: 3600 = 1 hour) + + Returns: + tuple: (success: bool, error_message: str) + """ + try: + # Build the install command based on package manager + if package_manager == "apt": + if user_password: + cmd = ["sudo", "-S", "apt-get", "install", "-y"] + packages + process = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + _, stderr = process.communicate( + input=f"{user_password}\n", timeout=timeout + ) + success = process.returncode == 0 + else: + cmd = ["sudo", "apt-get", "install", "-y"] + packages + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=timeout + ) + success = result.returncode == 0 + stderr = result.stderr + + elif package_manager == "dnf": + if user_password: + cmd = ["sudo", "-S", "dnf", "install", "-y"] + packages + process = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + _, stderr = process.communicate( + input=f"{user_password}\n", timeout=timeout + ) + success = process.returncode == 0 + else: + cmd = ["sudo", "dnf", "install", "-y"] + packages + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=timeout + ) + success = result.returncode == 0 + stderr = result.stderr + + elif package_manager == "pacman": + if user_password: + cmd = ["sudo", "-S", "pacman", "-S", "--noconfirm"] + packages + process = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + _, stderr = process.communicate( + input=f"{user_password}\n", timeout=timeout + ) + success = process.returncode == 0 + else: + cmd = ["sudo", "pacman", "-S", "--noconfirm"] + packages + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=timeout + ) + success = result.returncode == 0 + stderr = result.stderr + else: + return False, "Unknown package manager" + + if success: + return True, "" + else: + return False, stderr + + except subprocess.TimeoutExpired: + return False, "Installation timed out" + except Exception as e: + return False, str(e) diff --git a/Framework/install_handler/linux/xwd.py b/Framework/install_handler/linux/xwd.py new file mode 100644 index 000000000..bf525a9f8 --- /dev/null +++ b/Framework/install_handler/linux/xwd.py @@ -0,0 +1,142 @@ +from Framework.install_handler.utils import send_response +from .linux_utils import ( + detect_package_manager, + check_all_packages_installed, + install_packages, +) + + +# Package definitions for different package managers +PACKAGES = { + "apt": [ + "x11-apps", # provides xwd + "imagemagick", # provides convert, import + "wmctrl", + ], + "dnf": [ + "xorg-x11-utils", # provides xwd on some distros + "ImageMagick", + "wmctrl", + ], + "pacman": [ + "imagemagick", + "wmctrl", + ], +} + + +async def check_status(): + """Checks if X Window utilities are installed.""" + + package_manager, _ = detect_package_manager() + + if not package_manager: + await send_response( + { + "action": "status", + "data": { + "category": "Linux Desktop", + "name": "X Window Utilities", + "status": "error", + "comment": "Unsupported package manager. Only apt, dnf, and pacman are supported.", + }, + } + ) + return False + + packages = PACKAGES.get(package_manager, []) + if check_all_packages_installed(package_manager, packages): + await send_response( + { + "action": "status", + "data": { + "category": "Linux Desktop", + "name": "X Window Utilities", + "status": "installed", + "comment": "X Window utilities are installed.", + }, + } + ) + return True + else: + await send_response( + { + "action": "status", + "data": { + "category": "Linux Desktop", + "name": "X Window Utilities", + "status": "not installed", + "comment": f"Install X Window utilities using {package_manager}.", + }, + } + ) + return False + + +async def install(user_password: str = ""): + """Install X Window utilities using the system package manager.""" + + is_already_installed = await check_status() + + if not is_already_installed: + package_manager, _ = detect_package_manager() + + if not package_manager: + await send_response( + { + "action": "status", + "data": { + "category": "Linux Desktop", + "name": "X Window Utilities", + "status": "error", + "comment": "Unsupported package manager. Only apt, dnf, and pacman are supported.", + }, + } + ) + return False + + packages = PACKAGES.get(package_manager, []) + await send_response( + { + "action": "status", + "data": { + "category": "Linux Desktop", + "name": "X Window Utilities", + "status": "installing", + "comment": f"Installing packages using {package_manager}, please wait...", + }, + } + ) + + success, error_msg = install_packages( + package_manager, packages, user_password, timeout=300 + ) + + if success: + await send_response( + { + "action": "status", + "data": { + "category": "Linux Desktop", + "name": "X Window Utilities", + "status": "installed", + "comment": "X Window utilities have been installed successfully.", + }, + } + ) + return True + else: + await send_response( + { + "action": "status", + "data": { + "category": "Linux Desktop", + "name": "X Window Utilities", + "status": "error", + "comment": f"Installation failed. Error: {error_msg}", + }, + } + ) + return False + else: + return True diff --git a/Framework/install_handler/route.py b/Framework/install_handler/route.py index 203c16ffd..6261187b7 100644 --- a/Framework/install_handler/route.py +++ b/Framework/install_handler/route.py @@ -6,6 +6,7 @@ from .ios import xcode from .database import postgresql, mysql, mariadb, oracle from .windows import inspector +from .linux import atspi, xwd services = [ { @@ -18,9 +19,9 @@ "install_text": "install", "os": ["windows", "linux", "darwin"], "status_function": chrome_for_testing.check_status, - "install_function": chrome_for_testing.install + "install_function": chrome_for_testing.install, } - ] + ], }, { "category": "Android", @@ -32,7 +33,7 @@ "install_text": "install", "os": ["windows", "linux", "darwin"], "status_function": adb.check_status, - "install_function": adb.install + "install_function": adb.install, }, { "name": "Node js 22", @@ -41,7 +42,7 @@ "install_text": "install", "os": ["windows", "linux", "darwin"], "status_function": node_js_22.check_status, - "install_function": node_js_22.install + "install_function": node_js_22.install, }, { "name": "Appium", @@ -50,7 +51,7 @@ "install_text": "install", "os": ["windows", "linux", "darwin"], "status_function": appium.check_status, - "install_function": appium.install + "install_function": appium.install, }, { "name": "Java", @@ -59,7 +60,7 @@ "install_text": "install", "os": ["windows", "linux", "darwin"], "status_function": java.check_status, - "install_function": java.install + "install_function": java.install, }, { "name": "Android Emulator", @@ -68,9 +69,9 @@ "install_text": "install", "os": ["windows", "linux", "darwin"], "status_function": android_emulator.check_status, - "install_function": android_emulator.install - } - ] + "install_function": android_emulator.install, + }, + ], }, { "category": "iOS", @@ -82,9 +83,9 @@ "install_text": "install", "os": ["darwin"], "status_function": xcode.check_status, - "install_function": xcode.install + "install_function": xcode.install, } - ] + ], }, { "category": "Database", @@ -96,7 +97,7 @@ "install_text": "install", "os": ["windows", "linux", "darwin"], "status_function": postgresql.check_status, - "install_function": postgresql.install + "install_function": postgresql.install, }, { "name": "MySQL", @@ -105,7 +106,7 @@ "install_text": "install", "os": ["windows", "linux", "darwin"], "status_function": mysql.check_status, - "install_function": mysql.install + "install_function": mysql.install, }, { "name": "MariaDB", @@ -114,7 +115,7 @@ "install_text": "install", "os": ["windows", "linux", "darwin"], "status_function": mariadb.check_status, - "install_function": mariadb.install + "install_function": mariadb.install, }, { "name": "Oracle", @@ -123,9 +124,9 @@ "install_text": "install", "os": ["windows", "linux", "darwin"], "status_function": oracle.check_status, - "install_function": oracle.install - } - ] + "install_function": oracle.install, + }, + ], }, { "category": "Windows", @@ -137,23 +138,51 @@ "install_text": "install", "os": ["windows"], "status_function": inspector.check_status, - "install_function": inspector.install + "install_function": inspector.install, } - ] - } + ], + }, + { + "category": "Linux", + "services": [ + { + "name": "AT-SPI Packages", + "status": "none", + "comment": "AT-SPI development packages for Linux accessibility automation.", + "install_text": "install", + "os": ["linux"], + "status_function": atspi.check_status, + "install_function": atspi.install, + "user_password": "yes", + }, + { + "name": "X Window Utilities", + "status": "none", + "comment": "X Window utilities including xwd, imagemagick, and wmctrl.", + "install_text": "install", + "os": ["linux"], + "status_function": xwd.check_status, + "install_function": xwd.install, + "user_password": "yes", + }, + ], + }, ] + class Item(BaseModel): name: str category: str + class Value(BaseModel): - model_config = ConfigDict(extra='forbid') - + model_config = ConfigDict(extra="forbid") + action: Literal["services_list", "install", "status"] item: Item | None = None + class Response(BaseModel): - model_config = ConfigDict(extra='forbid') - - value: Value | None \ No newline at end of file + model_config = ConfigDict(extra="forbid") + + value: Value | None From 7e3ccae7faa21444aeb1fc8b6c1a4af67331c2f7 Mon Sep 17 00:00:00 2001 From: Mahmudul Alam Date: Sun, 30 Nov 2025 15:11:47 +0600 Subject: [PATCH 02/17] Updated Category name and Service name --- Framework/install_handler/linux/atspi.py | 16 +++++----- Framework/install_handler/linux/xwd.py | 38 ++++++++++++------------ Framework/install_handler/route.py | 4 +-- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/Framework/install_handler/linux/atspi.py b/Framework/install_handler/linux/atspi.py index 39130f126..149607d0a 100644 --- a/Framework/install_handler/linux/atspi.py +++ b/Framework/install_handler/linux/atspi.py @@ -39,6 +39,7 @@ async def check_status(): """Checks if AT-SPI development packages are installed.""" + print("Checking AT-SPI development packages status...") package_manager, _ = detect_package_manager() @@ -47,7 +48,7 @@ async def check_status(): { "action": "status", "data": { - "category": "Linux Accessibility", + "category": "Linux", "name": "AT-SPI Packages", "status": "error", "comment": "Unsupported package manager. Only apt, dnf, and pacman are supported.", @@ -62,7 +63,7 @@ async def check_status(): { "action": "status", "data": { - "category": "Linux Accessibility", + "category": "Linux", "name": "AT-SPI Packages", "status": "installed", "comment": "AT-SPI development packages are installed.", @@ -75,7 +76,7 @@ async def check_status(): { "action": "status", "data": { - "category": "Linux Accessibility", + "category": "Linux", "name": "AT-SPI Packages", "status": "not installed", "comment": f"Install AT-SPI packages using {package_manager}.", @@ -87,6 +88,7 @@ async def check_status(): async def install(user_password: str = ""): """Install AT-SPI development packages using the system package manager.""" + print("Installing AT-SPI development packages...") is_already_installed = await check_status() @@ -98,7 +100,7 @@ async def install(user_password: str = ""): { "action": "status", "data": { - "category": "Linux Accessibility", + "category": "Linux", "name": "AT-SPI Packages", "status": "error", "comment": "Unsupported package manager. Only apt, dnf, and pacman are supported.", @@ -112,7 +114,7 @@ async def install(user_password: str = ""): { "action": "status", "data": { - "category": "Linux Accessibility", + "category": "Linux", "name": "AT-SPI Packages", "status": "installing", "comment": f"Installing packages using {package_manager}, please wait...", @@ -129,7 +131,7 @@ async def install(user_password: str = ""): { "action": "status", "data": { - "category": "Linux Accessibility", + "category": "Linux", "name": "AT-SPI Packages", "status": "installed", "comment": "AT-SPI packages have been installed successfully.", @@ -142,7 +144,7 @@ async def install(user_password: str = ""): { "action": "status", "data": { - "category": "Linux Accessibility", + "category": "Linux", "name": "AT-SPI Packages", "status": "error", "comment": f"Installation failed. Error: {error_msg}", diff --git a/Framework/install_handler/linux/xwd.py b/Framework/install_handler/linux/xwd.py index bf525a9f8..a2d1a2753 100644 --- a/Framework/install_handler/linux/xwd.py +++ b/Framework/install_handler/linux/xwd.py @@ -26,7 +26,7 @@ async def check_status(): - """Checks if X Window utilities are installed.""" + """Checks if Screen Capture Utilities are installed.""" package_manager, _ = detect_package_manager() @@ -35,8 +35,8 @@ async def check_status(): { "action": "status", "data": { - "category": "Linux Desktop", - "name": "X Window Utilities", + "category": "Linux", + "name": "Screen Capture Utilities", "status": "error", "comment": "Unsupported package manager. Only apt, dnf, and pacman are supported.", }, @@ -50,10 +50,10 @@ async def check_status(): { "action": "status", "data": { - "category": "Linux Desktop", - "name": "X Window Utilities", + "category": "Linux", + "name": "Screen Capture Utilities", "status": "installed", - "comment": "X Window utilities are installed.", + "comment": "Screen Capture Utilities are installed.", }, } ) @@ -63,10 +63,10 @@ async def check_status(): { "action": "status", "data": { - "category": "Linux Desktop", - "name": "X Window Utilities", + "category": "Linux", + "name": "Screen Capture Utilities", "status": "not installed", - "comment": f"Install X Window utilities using {package_manager}.", + "comment": f"Install Screen Capture Utilities using {package_manager}.", }, } ) @@ -74,7 +74,7 @@ async def check_status(): async def install(user_password: str = ""): - """Install X Window utilities using the system package manager.""" + """Install Screen Capture Utilities using the system package manager.""" is_already_installed = await check_status() @@ -86,8 +86,8 @@ async def install(user_password: str = ""): { "action": "status", "data": { - "category": "Linux Desktop", - "name": "X Window Utilities", + "category": "Linux", + "name": "Screen Capture Utilities", "status": "error", "comment": "Unsupported package manager. Only apt, dnf, and pacman are supported.", }, @@ -100,8 +100,8 @@ async def install(user_password: str = ""): { "action": "status", "data": { - "category": "Linux Desktop", - "name": "X Window Utilities", + "category": "Linux", + "name": "Screen Capture Utilities", "status": "installing", "comment": f"Installing packages using {package_manager}, please wait...", }, @@ -117,10 +117,10 @@ async def install(user_password: str = ""): { "action": "status", "data": { - "category": "Linux Desktop", - "name": "X Window Utilities", + "category": "Linux", + "name": "Screen Capture Utilities", "status": "installed", - "comment": "X Window utilities have been installed successfully.", + "comment": "Screen Capture Utilities have been installed successfully.", }, } ) @@ -130,8 +130,8 @@ async def install(user_password: str = ""): { "action": "status", "data": { - "category": "Linux Desktop", - "name": "X Window Utilities", + "category": "Linux", + "name": "Screen Capture Utilities", "status": "error", "comment": f"Installation failed. Error: {error_msg}", }, diff --git a/Framework/install_handler/route.py b/Framework/install_handler/route.py index 6261187b7..c21ed31e1 100644 --- a/Framework/install_handler/route.py +++ b/Framework/install_handler/route.py @@ -156,9 +156,9 @@ "user_password": "yes", }, { - "name": "X Window Utilities", + "name": "Screen Capture Utilities", "status": "none", - "comment": "X Window utilities including xwd, imagemagick, and wmctrl.", + "comment": "Screen Capture Utilities including xwd, imagemagick, and wmctrl.", "install_text": "install", "os": ["linux"], "status_function": xwd.check_status, From 7aa3d84f2719932a24288137f59c554bcc229e14 Mon Sep 17 00:00:00 2001 From: Mahmudul Alam Date: Mon, 1 Dec 2025 09:53:53 +0600 Subject: [PATCH 03/17] Added iOS and macOS installation codes --- Framework/install_handler/ios/xcode.py | 12 +- Framework/install_handler/macos/common.py | 171 ++++++++++++++++++++++ Framework/install_handler/macos/xcode.py | 16 ++ Framework/install_handler/route.py | 16 ++ 4 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 Framework/install_handler/macos/common.py create mode 100644 Framework/install_handler/macos/xcode.py diff --git a/Framework/install_handler/ios/xcode.py b/Framework/install_handler/ios/xcode.py index 96b9ac388..08ebddee1 100644 --- a/Framework/install_handler/ios/xcode.py +++ b/Framework/install_handler/ios/xcode.py @@ -1,7 +1,15 @@ -async def check_status(): +from Framework.install_handler.macos.common import xcode_check_status, xcode_install + + +async def check_status() -> bool: + """Check if Xcode is installed and license is accepted.""" print("[xcode] Checking status...") + return await xcode_check_status("iOS") + -async def install(): +async def install(user_password: str = "") -> bool: + """Install Xcode via App Store and accept license.""" print("[xcode] Installing...") + return await xcode_install("iOS", user_password) diff --git a/Framework/install_handler/macos/common.py b/Framework/install_handler/macos/common.py new file mode 100644 index 000000000..f5e3b5ef9 --- /dev/null +++ b/Framework/install_handler/macos/common.py @@ -0,0 +1,171 @@ +import asyncio +import platform +import os +import shutil +import subprocess +from Framework.install_handler.utils import send_response + + +async def _send_status(category, status: str, comment: str): + """Helper to send status responses.""" + await send_response( + { + "action": "status", + "data": { + "category": "MacOS", + "name": "Xcode", + "status": status, + "comment": comment, + }, + } + ) + + +async def xcode_check_status(category) -> bool: + """Check if Xcode is installed and license is accepted.""" + print("[xcode] Checking status...") + + if platform.system().lower() != "darwin": + await _send_status( + category, "error", "Unsupported OS. Xcode is only available on macOS." + ) + return False + + xcodebuild_path = shutil.which("xcodebuild") + if not xcodebuild_path or not os.path.exists("/Applications/Xcode.app"): + await _send_status(category, "not installed", "Xcode is not installed.") + return False + + try: + # Check version + result = subprocess.run( + [xcodebuild_path, "-version"], capture_output=True, text=True, timeout=30 + ) + if result.returncode != 0: + await _send_status( + category, "error", f"xcodebuild error: {result.stderr.strip()}" + ) + return False + + version = result.stdout.splitlines()[0].strip() if result.stdout else "unknown" + + # Check license + license_result = subprocess.run( + [xcodebuild_path, "-checkFirstLaunchStatus"], + capture_output=True, + text=True, + timeout=15, + ) + + if license_result.returncode == 0: + await _send_status(category, "installed", f"{version}") + return True + else: + await _send_status( + category, + "not installed", + f"{version} installed but license not accepted.", + ) + return False + + except subprocess.TimeoutExpired: + await _send_status(category, "error", "xcodebuild timed out.") + return False + except Exception as e: + await _send_status(category, "error", f"Error checking Xcode: {e}") + return False + + +async def _accept_license(category, user_password: str) -> bool: + """Attempt to accept Xcode license using sudo.""" + xcodebuild_path = shutil.which("xcodebuild") or "xcodebuild" + + if not user_password: + await _send_status( + category, "error", "Password required to accept Xcode license." + ) + return False + + try: + result = subprocess.run( + ["sudo", "-S", xcodebuild_path, "-license", "accept"], + input=user_password + "\n", + capture_output=True, + text=True, + timeout=60, + ) + + if result.returncode == 0: + await _send_status( + category, "installed", "Xcode license accepted successfully." + ) + return True + else: + await _send_status( + category, "error", f"Failed to accept license: {result.stderr.strip()}" + ) + return False + except Exception as e: + await _send_status(category, "error", f"Error accepting license: {e}") + return False + + +async def xcode_install(category, user_password: str = "") -> bool: + """Install Xcode via App Store and accept license.""" + print("[xcode] Installing...") + + if platform.system().lower() != "darwin": + await _send_status( + category, "error", "Unsupported OS. Xcode is only available on macOS." + ) + return False + + if await xcode_check_status(category): + return True + + # Open App Store + await _send_status( + category, "installing", "Opening App Store. Please install Xcode and wait..." + ) + try: + subprocess.run( + ["open", "itms-apps://itunes.apple.com/app/id497799835"], timeout=10 + ) + except Exception: + pass + + # Poll for installation (2 hours timeout) + timeout_seconds = 2 * 60 * 60 + interval = 10 + elapsed = 0 + + while elapsed < timeout_seconds: + await asyncio.sleep(interval) + elapsed += interval + + # Check if Xcode app exists + xcodebuild_path = shutil.which("xcodebuild") + if os.path.exists("/Applications/Xcode.app") and xcodebuild_path: + # Check license status + try: + license_result = subprocess.run( + [xcodebuild_path, "-checkFirstLaunchStatus"], + capture_output=True, + text=True, + timeout=15, + ) + + if license_result.returncode == 0: + await _send_status( + category, "installed", "Xcode installed successfully." + ) + return True + else: + # License not accepted, attempt to accept + return await _accept_license(category, user_password) + except Exception: + # Try to accept license anyway + return await _accept_license(category, user_password) + + await _send_status(category, "error", "Timed out waiting for Xcode installation.") + return False diff --git a/Framework/install_handler/macos/xcode.py b/Framework/install_handler/macos/xcode.py new file mode 100644 index 000000000..a940e0834 --- /dev/null +++ b/Framework/install_handler/macos/xcode.py @@ -0,0 +1,16 @@ +from .common import xcode_check_status, xcode_install + + +async def check_status() -> bool: + """Check if Xcode is installed and license is accepted.""" + print("[xcode] Checking status...") + + return await xcode_check_status("MacOS") + + + +async def install(user_password: str = "") -> bool: + """Install Xcode via App Store and accept license.""" + print("[xcode] Installing...") + + return await xcode_install("MacOS", user_password) diff --git a/Framework/install_handler/route.py b/Framework/install_handler/route.py index c21ed31e1..f7dd868ef 100644 --- a/Framework/install_handler/route.py +++ b/Framework/install_handler/route.py @@ -84,6 +84,22 @@ "os": ["darwin"], "status_function": xcode.check_status, "install_function": xcode.install, + "user_password": "yes", + } + ], + }, + { + "category": "MacOS", + "services": [ + { + "name": "Xcode", + "status": "none", + "comment": "Xcode is a tool for managing Xcode devices.", + "install_text": "install", + "os": ["darwin"], + "status_function": xcode.check_status, + "install_function": xcode.install, + "user_password": "yes", } ], }, From 3039fb795c74c99c57a5a51abe8c4f4686563462 Mon Sep 17 00:00:00 2001 From: Mahmudul Alam Date: Mon, 1 Dec 2025 10:00:25 +0600 Subject: [PATCH 04/17] fix: category parameter was not being used --- Framework/install_handler/macos/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Framework/install_handler/macos/common.py b/Framework/install_handler/macos/common.py index f5e3b5ef9..29e593d29 100644 --- a/Framework/install_handler/macos/common.py +++ b/Framework/install_handler/macos/common.py @@ -12,7 +12,7 @@ async def _send_status(category, status: str, comment: str): { "action": "status", "data": { - "category": "MacOS", + "category": category, "name": "Xcode", "status": status, "comment": comment, From e590d53266a560b6fc4b54bfa42e8c1e4fa6d219 Mon Sep 17 00:00:00 2001 From: Mahmudul Alam Date: Mon, 1 Dec 2025 10:04:46 +0600 Subject: [PATCH 05/17] fixed macos_status not working --- Framework/install_handler/route.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Framework/install_handler/route.py b/Framework/install_handler/route.py index f7dd868ef..ccd0da87f 100644 --- a/Framework/install_handler/route.py +++ b/Framework/install_handler/route.py @@ -4,6 +4,7 @@ from .web import chrome_for_testing from .android import adb, node_js_22, appium, java, android_emulator from .ios import xcode +from .macos import xcode as macos_xcode from .database import postgresql, mysql, mariadb, oracle from .windows import inspector from .linux import atspi, xwd @@ -97,8 +98,8 @@ "comment": "Xcode is a tool for managing Xcode devices.", "install_text": "install", "os": ["darwin"], - "status_function": xcode.check_status, - "install_function": xcode.install, + "status_function": macos_xcode.check_status, + "install_function": macos_xcode.install, "user_password": "yes", } ], From ec01c10fc58cc5fbee0ccf19eb6d0ee93882717b Mon Sep 17 00:00:00 2001 From: Mahmudul Alam Date: Thu, 4 Dec 2025 12:24:28 +0600 Subject: [PATCH 06/17] feat: added multiple android device support --- .../Mobile/CrossPlatform/Appium/BuiltInFunctions.py | 1 + Framework/MainDriverApi.py | 1 + 2 files changed, 2 insertions(+) diff --git a/Framework/Built_In_Automation/Mobile/CrossPlatform/Appium/BuiltInFunctions.py b/Framework/Built_In_Automation/Mobile/CrossPlatform/Appium/BuiltInFunctions.py index ae3ae20f3..32705eb4c 100755 --- a/Framework/Built_In_Automation/Mobile/CrossPlatform/Appium/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Mobile/CrossPlatform/Appium/BuiltInFunctions.py @@ -933,6 +933,7 @@ def start_appium_driver( if str(appium_details[device_id]["type"]).lower() == "android": # All Desired caps = https://appium.io/docs/en/writing-running-appium/caps/ desired_caps["platformName"] = appium_details[device_id]["type"] # Set platform name + desired_caps["udid"] = appium_details[device_id]["serial"] desired_caps["autoLaunch"] = "false" # Do not launch application desired_caps["fullReset"] = "false" # Do not reinstall application if no_reset: diff --git a/Framework/MainDriverApi.py b/Framework/MainDriverApi.py index 31a0efff9..f4dca8d34 100644 --- a/Framework/MainDriverApi.py +++ b/Framework/MainDriverApi.py @@ -1260,6 +1260,7 @@ def send_dom_variables(): def set_device_info_according_to_user_order(device_order, device_dict, test_case_no, test_case_name, user_info_object, Userid, **kwargs): # Need to set device_info for browserstack here global device_info + device_order = [[i, i] for i in range(1, len(device_dict) + 1)] # overriding zsvc return device order. zsvc only returns [[1,1]] without considering the number of devices available shared.Set_Shared_Variables("device_order", device_order) if isinstance(device_order, list): try: From 7631a46fb16833b197ff9a55a228e074b7378339 Mon Sep 17 00:00:00 2001 From: Mahmudul Alam Date: Thu, 4 Dec 2025 16:39:38 +0600 Subject: [PATCH 07/17] feat: show device name in server and allow selective inspection --- server/mobile.py | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/server/mobile.py b/server/mobile.py index 55dbc10ed..2f04967c7 100644 --- a/server/mobile.py +++ b/server/mobile.py @@ -2,7 +2,6 @@ import os import subprocess import base64 -import time from typing import Literal import asyncio @@ -32,6 +31,7 @@ class DeviceInfo(BaseModel): """Model for device information.""" serial: str status: str + name: str | None = None # model: str | None = None # product: str | None = None @@ -42,33 +42,34 @@ def get_devices(): # Get list of devices devices_output = run_adb_command(f"{ADB_PATH} devices -l") devices = [] - + # Parse adb devices output - for line in devices_output.split('\n')[1:]: # Skip first line (header) + index = 1 + for line in devices_output.split("\n")[1:]: # Skip first line (header) if line.strip(): parts = line.split() if len(parts) >= 2: serial = parts[0] status = parts[1] + name = f"device {index}" + index += 1 # model = run_adb_command(f"{ADB_PATH} -s {serial} shell getprop ro.product.model") # product = run_adb_command(f"{ADB_PATH} -s {serial} shell getprop ro.product.name") - devices.append(DeviceInfo( - serial=serial, - status=status - )) - + devices.append(DeviceInfo(serial=serial, status=status, name=name)) + return devices except Exception as e: return [] + @router.get("/inspect") -def inspect(): +def inspect(device_serial: str | None = None): """Get the Mobile DOM and screenshot.""" try: # Capture UI and screenshot - capture_ui_dump() - capture_screenshot() + capture_ui_dump(device_serial=device_serial) + capture_screenshot(device_serial=device_serial) # Read XML file with open(UI_XML_PATH, 'r') as xml_file: @@ -108,9 +109,12 @@ def run_adb_command(command): return f"Error: {e.stderr.strip()}" -def capture_ui_dump(): +def capture_ui_dump(device_serial: str | None = None): """Capture the current UI hierarchy from the device""" - out = run_adb_command(f"{ADB_PATH} shell uiautomator dump /sdcard/ui.xml") + device_flag = f"-s {device_serial}" if device_serial else "" + out = run_adb_command( + f"{ADB_PATH} {device_flag} shell uiautomator dump /sdcard/ui.xml".strip() + ) if out.startswith("Error:"): from Framework.Built_In_Automation.Mobile.CrossPlatform.Appium.BuiltInFunctions import appium_driver if appium_driver is None: @@ -119,14 +123,19 @@ def capture_ui_dump(): with open(UI_XML_PATH, 'w') as xml_file: xml_file.write(page_src) else: - out = run_adb_command(f"{ADB_PATH} pull /sdcard/ui.xml {UI_XML_PATH}") + out = run_adb_command( + f"{ADB_PATH} {device_flag} pull /sdcard/ui.xml {UI_XML_PATH}" + ) if out.startswith("Error:"): return -def capture_screenshot(): +def capture_screenshot(device_serial: str | None = None): """Capture the current UI hierarchy from the device""" - out = run_adb_command(f"{ADB_PATH} shell screencap -p /sdcard/screen.png") + device_flag = f"-s {device_serial}" if device_serial else "" + out = run_adb_command( + f"{ADB_PATH} {device_flag} shell screencap -p /sdcard/screen.png".strip() + ) if out.startswith("Error:"): from Framework.Built_In_Automation.Mobile.CrossPlatform.Appium.BuiltInFunctions import appium_driver if appium_driver is None: @@ -134,7 +143,9 @@ def capture_screenshot(): full_screenshot_path = os.path.join(os.getcwd(), SCREENSHOT_PATH) appium_driver.save_screenshot(full_screenshot_path) else: - out = run_adb_command(f"{ADB_PATH} pull /sdcard/screen.png {SCREENSHOT_PATH}") + out = run_adb_command( + f"{ADB_PATH} {device_flag} pull /sdcard/screen.png {SCREENSHOT_PATH}" + ) if out.startswith("Error:"): return From 27b4f37c4de90f5af51042f8980a91b6fa4ac893 Mon Sep 17 00:00:00 2001 From: Mahmudul Alam Date: Sun, 7 Dec 2025 07:10:14 +0600 Subject: [PATCH 08/17] feat: Restrict AT-SPI and XWD installations to X11 sessions only. --- Framework/install_handler/linux/atspi.py | 33 ++++++++++++++++++++++++ Framework/install_handler/linux/xwd.py | 33 ++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/Framework/install_handler/linux/atspi.py b/Framework/install_handler/linux/atspi.py index 149607d0a..9df1493d8 100644 --- a/Framework/install_handler/linux/atspi.py +++ b/Framework/install_handler/linux/atspi.py @@ -1,3 +1,4 @@ +import os from Framework.install_handler.utils import send_response from .linux_utils import ( detect_package_manager, @@ -41,6 +42,22 @@ async def check_status(): """Checks if AT-SPI development packages are installed.""" print("Checking AT-SPI development packages status...") + # Check if session type is X11 + session_type = os.environ.get("XDG_SESSION_TYPE", "unknown") + if session_type != "x11": + await send_response( + { + "action": "status", + "data": { + "category": "Linux", + "name": "AT-SPI Packages", + "status": "error", + "comment": f"Only X11 is supported. Current session type: {session_type}.", + }, + } + ) + return False + package_manager, _ = detect_package_manager() if not package_manager: @@ -90,6 +107,22 @@ async def install(user_password: str = ""): """Install AT-SPI development packages using the system package manager.""" print("Installing AT-SPI development packages...") + # Check if session type is X11 + session_type = os.environ.get("XDG_SESSION_TYPE", "unknown") + if session_type != "x11": + await send_response( + { + "action": "status", + "data": { + "category": "Linux", + "name": "AT-SPI Packages", + "status": "error", + "comment": f"Only X11 is supported. Current session type: {session_type}.", + }, + } + ) + return False + is_already_installed = await check_status() if not is_already_installed: diff --git a/Framework/install_handler/linux/xwd.py b/Framework/install_handler/linux/xwd.py index a2d1a2753..561a5e20e 100644 --- a/Framework/install_handler/linux/xwd.py +++ b/Framework/install_handler/linux/xwd.py @@ -1,3 +1,4 @@ +import os from Framework.install_handler.utils import send_response from .linux_utils import ( detect_package_manager, @@ -28,6 +29,22 @@ async def check_status(): """Checks if Screen Capture Utilities are installed.""" + # Check if session type is X11 + session_type = os.environ.get("XDG_SESSION_TYPE", "unknown") + if session_type != "x11": + await send_response( + { + "action": "status", + "data": { + "category": "Linux", + "name": "Screen Capture Utilities", + "status": "error", + "comment": f"Only X11 is supported. Current session type: {session_type}.", + }, + } + ) + return False + package_manager, _ = detect_package_manager() if not package_manager: @@ -76,6 +93,22 @@ async def check_status(): async def install(user_password: str = ""): """Install Screen Capture Utilities using the system package manager.""" + # Check if session type is X11 + session_type = os.environ.get("XDG_SESSION_TYPE", "unknown") + if session_type != "x11": + await send_response( + { + "action": "status", + "data": { + "category": "Linux", + "name": "Screen Capture Utilities", + "status": "error", + "comment": f"Only X11 is supported. Current session type: {session_type}.", + }, + } + ) + return False + is_already_installed = await check_status() if not is_already_installed: From d557ae362bc35a07337abd39ebd06e4800322de4 Mon Sep 17 00:00:00 2001 From: Mahmudul Alam Date: Sun, 7 Dec 2025 09:03:30 +0600 Subject: [PATCH 09/17] feat: Add iOS Simulator and WebDriverAgent installation support --- Framework/install_handler/ios/simulator.py | 227 ++++++++++++++ Framework/install_handler/ios/webdriver.py | 338 +++++++++++++++++++++ Framework/install_handler/route.py | 21 +- 3 files changed, 585 insertions(+), 1 deletion(-) create mode 100644 Framework/install_handler/ios/simulator.py create mode 100644 Framework/install_handler/ios/webdriver.py diff --git a/Framework/install_handler/ios/simulator.py b/Framework/install_handler/ios/simulator.py new file mode 100644 index 000000000..93c92c8e6 --- /dev/null +++ b/Framework/install_handler/ios/simulator.py @@ -0,0 +1,227 @@ +import asyncio +import platform +import os +import shutil +import subprocess +from Framework.install_handler.utils import send_response + + +async def _send_status(status: str, comment: str): + """Helper to send status responses.""" + await send_response( + { + "action": "status", + "data": { + "category": "iOS", + "name": "Simulator", + "status": status, + "comment": comment, + }, + } + ) + + +async def check_status() -> bool: + """Check if iOS Simulator is installed and available.""" + print("[simulator] Checking status...") + + if platform.system().lower() != "darwin": + await _send_status( + "error", "Unsupported OS. iOS Simulator is only available on macOS." + ) + return False + + # Check if Xcode is installed first (required for simulators) + if not os.path.exists("/Applications/Xcode.app"): + await _send_status( + "not installed", "Xcode must be installed before using iOS Simulator." + ) + return False + + # Check if simctl is available + simctl_path = shutil.which("xcrun") + if not simctl_path: + await _send_status( + "not installed", "xcrun not found. Xcode command line tools may not be installed." + ) + return False + + try: + # Check if any simulators are available + result = subprocess.run( + ["xcrun", "simctl", "list", "devices", "available"], + capture_output=True, + text=True, + timeout=30, + ) + + if result.returncode != 0: + await _send_status( + "error", f"simctl error: {result.stderr.strip()}" + ) + return False + + # Check if there are any iOS simulators available + output = result.stdout + if "iOS" in output or "iPhone" in output or "iPad" in output: + # Count number of available devices + device_lines = [line for line in output.splitlines() if "(" in line and ")" in line and "Booted" not in line] + await _send_status( + "installed", f"iOS Simulator available with {len(device_lines)} devices." + ) + return True + else: + await _send_status( + "not installed", "No iOS Simulator devices found. Install Xcode or simulator runtimes." + ) + return False + + except subprocess.TimeoutExpired: + await _send_status("error", "simctl command timed out.") + return False + except Exception as e: + await _send_status("error", f"Error checking iOS Simulator: {e}") + return False + + +async def _install_command_line_tools(user_password: str) -> bool: + """Install Xcode command line tools if not present.""" + try: + # Check if already installed + result = subprocess.run( + ["xcode-select", "-p"], + capture_output=True, + text=True, + timeout=10, + ) + + if result.returncode == 0: + return True + + # Install command line tools + await _send_status( + "installing", "Installing Xcode command line tools, please wait..." + ) + + install_result = subprocess.run( + ["xcode-select", "--install"], + capture_output=True, + text=True, + timeout=10, + ) + + # Wait for installation to complete (poll for up to 30 minutes) + timeout_seconds = 30 * 60 + interval = 10 + elapsed = 0 + + while elapsed < timeout_seconds: + await asyncio.sleep(interval) + elapsed += interval + + check_result = subprocess.run( + ["xcode-select", "-p"], + capture_output=True, + text=True, + timeout=10, + ) + + if check_result.returncode == 0: + return True + + await _send_status("error", "Timed out waiting for command line tools installation.") + return False + + except Exception as e: + await _send_status("error", f"Error installing command line tools: {e}") + return False + + +async def _install_simulator_runtime() -> bool: + """Install iOS Simulator runtime if missing.""" + try: + # First, ensure Xcode command line tools are set correctly + result = subprocess.run( + ["sudo", "xcode-select", "--switch", "/Applications/Xcode.app/Contents/Developer"], + capture_output=True, + text=True, + timeout=30, + ) + + # Check available simulator runtimes + await _send_status( + "installing", "Checking for available iOS Simulator runtimes..." + ) + + runtime_result = subprocess.run( + ["xcrun", "simctl", "list", "runtimes"], + capture_output=True, + text=True, + timeout=30, + ) + + if runtime_result.returncode != 0: + await _send_status( + "error", f"Failed to list runtimes: {runtime_result.stderr.strip()}" + ) + return False + + # If no iOS runtimes found, guide user to install via Xcode + if "iOS" not in runtime_result.stdout: + await _send_status( + "error", + "No iOS runtimes found. Please install via Xcode > Settings > Platforms." + ) + return False + + return True + + except Exception as e: + await _send_status("error", f"Error installing simulator runtime: {e}") + return False + + +async def install(user_password: str = "") -> bool: + """Install iOS Simulator (requires Xcode to be installed first).""" + print("[simulator] Installing...") + + if platform.system().lower() != "darwin": + await _send_status( + "error", "Unsupported OS. iOS Simulator is only available on macOS." + ) + return False + + # Check if already installed and working + if await check_status(): + return True + + # Ensure Xcode is installed + if not os.path.exists("/Applications/Xcode.app"): + await _send_status( + "error", "Xcode must be installed first. Please install Xcode from the App Store." + ) + return False + + await _send_status( + "installing", "Setting up iOS Simulator..." + ) + + # Install command line tools if needed + if not await _install_command_line_tools(user_password): + return False + + # Install/verify simulator runtime + if not await _install_simulator_runtime(): + return False + + # Final status check + if await check_status(): + await _send_status( + "installed", "iOS Simulator is ready to use." + ) + return True + else: + await _send_status( + "error", "iOS Simulator setup completed but verification failed." + ) + return False diff --git a/Framework/install_handler/ios/webdriver.py b/Framework/install_handler/ios/webdriver.py new file mode 100644 index 000000000..b51d27de9 --- /dev/null +++ b/Framework/install_handler/ios/webdriver.py @@ -0,0 +1,338 @@ +import platform +import os +import shutil +import subprocess +from pathlib import Path +from Framework.install_handler.utils import send_response + + +async def _send_status(status: str, comment: str): + """Helper to send status responses.""" + await send_response( + { + "action": "status", + "data": { + "category": "iOS", + "name": "WebDriver", + "status": status, + "comment": comment, + }, + } + ) + + +def _get_webdriver_path() -> Path: + """Get the path where WebDriverAgent should be installed.""" + home = Path.home() + return home / ".zeuz" / "WebDriverAgent" + + +async def _check_xcode_installed() -> bool: + """Check if Xcode is installed.""" + if not os.path.exists("/Applications/Xcode.app"): + return False + + xcodebuild_path = shutil.which("xcodebuild") + if not xcodebuild_path: + return False + + try: + result = subprocess.run( + [xcodebuild_path, "-version"], + capture_output=True, + text=True, + timeout=30, + ) + return result.returncode == 0 + except Exception: + return False + + +async def _get_available_simulator() -> str | None: + """Get the first available iOS simulator device name.""" + try: + result = subprocess.run( + ["xcrun", "simctl", "list", "devices", "available", "iOS"], + capture_output=True, + text=True, + timeout=30, + ) + + if result.returncode != 0: + return None + + # Parse output to find first iPhone or iPad simulator + for line in result.stdout.splitlines(): + line = line.strip() + # Look for device lines like "iPhone 16 Pro (UUID) (Shutdown)" + if "iPhone" in line and "(" in line: + # Extract device name (everything before first parenthesis) + device_name = line.split("(")[0].strip() + if device_name: + return device_name + + return None + except Exception: + return None + + +async def check_status() -> bool: + """Check if WebDriverAgent is installed and built.""" + print("[webdriver] Checking status...") + + if platform.system().lower() != "darwin": + await _send_status( + "error", "Unsupported OS. WebDriverAgent is only available on macOS." + ) + return False + + # Check if Xcode is installed + if not await _check_xcode_installed(): + await _send_status( + "not installed", "Xcode must be installed before using WebDriverAgent." + ) + return False + + webdriver_path = _get_webdriver_path() + project_path = webdriver_path / "WebDriverAgent.xcodeproj" + + # Check if WebDriverAgent is cloned + if not project_path.exists(): + await _send_status( + "not installed", "WebDriverAgent repository is not cloned." + ) + return False + + # Check if the project has been built + # Look for derived data or built products + try: + # Check if we can read the project + result = subprocess.run( + ["xcodebuild", "-project", str(project_path), "-list"], + capture_output=True, + text=True, + timeout=30, + cwd=str(webdriver_path), + ) + + if result.returncode != 0: + await _send_status( + "not installed", f"WebDriverAgent project is invalid: {result.stderr.strip()}" + ) + return False + + # Check if WebDriverAgentRunner scheme exists + if "WebDriverAgentRunner" in result.stdout: + await _send_status( + "installed", f"WebDriverAgent is installed at {webdriver_path}" + ) + return True + else: + await _send_status( + "not installed", "WebDriverAgentRunner scheme not found in project." + ) + return False + + except subprocess.TimeoutExpired: + await _send_status("error", "xcodebuild command timed out.") + return False + except Exception as e: + await _send_status("error", f"Error checking WebDriverAgent: {e}") + return False + + +async def _clone_repository(webdriver_path: Path) -> bool: + """Clone the WebDriverAgent repository.""" + try: + # Remove existing directory if it exists but is incomplete + if webdriver_path.exists(): + await _send_status( + "installing", "Removing incomplete WebDriverAgent installation..." + ) + shutil.rmtree(webdriver_path) + + # Create parent directory + webdriver_path.parent.mkdir(parents=True, exist_ok=True) + + await _send_status( + "installing", "Cloning WebDriverAgent repository, please wait..." + ) + + # Clone the repository + result = subprocess.run( + [ + "git", "clone", + "--depth", "1", # Shallow clone for faster download + "https://github.com/appium/WebDriverAgent.git", + str(webdriver_path) + ], + capture_output=True, + text=True, + timeout=600, # 10 minutes timeout + ) + + if result.returncode != 0: + await _send_status( + "error", f"Failed to clone repository: {result.stderr.strip()}" + ) + return False + + return True + + except subprocess.TimeoutExpired: + await _send_status("error", "Git clone timed out.") + return False + except Exception as e: + await _send_status("error", f"Error cloning repository: {e}") + return False + + +async def _bootstrap_webdriver(webdriver_path: Path) -> bool: + """Run the bootstrap script if it exists.""" + bootstrap_script = webdriver_path / "Scripts" / "bootstrap.sh" + + if not bootstrap_script.exists(): + # Try alternative path + bootstrap_script = webdriver_path / "bootstrap.sh" + + if bootstrap_script.exists(): + try: + await _send_status( + "installing", "Running WebDriverAgent bootstrap script..." + ) + + result = subprocess.run( + ["bash", str(bootstrap_script)], + capture_output=True, + text=True, + timeout=600, + cwd=str(webdriver_path), + ) + + if result.returncode != 0: + # Non-fatal, just log it + print(f"Bootstrap warning: {result.stderr.strip()}") + + return True + except Exception as e: + # Non-fatal + print(f"Bootstrap warning: {e}") + return True + + return True + + +async def _build_webdriver(webdriver_path: Path) -> bool: + """Build WebDriverAgent project.""" + try: + project_path = webdriver_path / "WebDriverAgent.xcodeproj" + + # Get available simulator + simulator_name = await _get_available_simulator() + if not simulator_name: + await _send_status( + "error", "No iOS Simulator found. Please install iOS Simulator first." + ) + return False + + await _send_status( + "installing", f"Building WebDriverAgent for {simulator_name}, please wait (this may take several minutes)..." + ) + + # Build the project + destination = f"platform=iOS Simulator,name={simulator_name}" + + result = subprocess.run( + [ + "xcodebuild", + "-project", str(project_path), + "-scheme", "WebDriverAgentRunner", + "-destination", destination, + "-allowProvisioningUpdates", + "build-for-testing", + ], + capture_output=True, + text=True, + timeout=1800, # 30 minutes timeout for build + cwd=str(webdriver_path), + ) + + if result.returncode != 0: + # Check if it's a signing error (common issue) + if "code signing" in result.stderr.lower() or "signing" in result.stderr.lower(): + await _send_status( + "error", + "Code signing error. Please open Xcode, go to WebDriverAgent project, " + "and configure signing in the target settings." + ) + else: + await _send_status( + "error", f"Build failed: {result.stderr.strip()[-500:]}" # Last 500 chars + ) + return False + + await _send_status( + "installed", f"WebDriverAgent built successfully for {simulator_name}" + ) + return True + + except subprocess.TimeoutExpired: + await _send_status("error", "Build timed out (exceeded 30 minutes).") + return False + except Exception as e: + await _send_status("error", f"Error building WebDriverAgent: {e}") + return False + + +async def install(user_password: str = "") -> bool: + """Install WebDriverAgent by cloning and building the project.""" + print("[webdriver] Installing...") + + if platform.system().lower() != "darwin": + await _send_status( + "error", "Unsupported OS. WebDriverAgent is only available on macOS." + ) + return False + + # Check if already installed + if await check_status(): + return True + + # Ensure Xcode is installed + if not await _check_xcode_installed(): + await _send_status( + "error", "Xcode must be installed first. Please install Xcode from the App Store." + ) + return False + + # Check if git is available + if not shutil.which("git"): + await _send_status( + "error", "Git is not installed. Please install git first." + ) + return False + + webdriver_path = _get_webdriver_path() + + # Clone repository + if not await _clone_repository(webdriver_path): + return False + + # Bootstrap (optional step) + await _bootstrap_webdriver(webdriver_path) + + # Build the project + if not await _build_webdriver(webdriver_path): + return False + + # Final verification + if await check_status(): + await _send_status( + "installed", f"WebDriverAgent is ready to use at {webdriver_path}" + ) + return True + else: + await _send_status( + "error", "Installation completed but verification failed." + ) + return False diff --git a/Framework/install_handler/route.py b/Framework/install_handler/route.py index ccd0da87f..83f896e31 100644 --- a/Framework/install_handler/route.py +++ b/Framework/install_handler/route.py @@ -3,7 +3,7 @@ from .web import chrome_for_testing from .android import adb, node_js_22, appium, java, android_emulator -from .ios import xcode +from .ios import xcode, simulator, webdriver from .macos import xcode as macos_xcode from .database import postgresql, mysql, mariadb, oracle from .windows import inspector @@ -86,6 +86,25 @@ "status_function": xcode.check_status, "install_function": xcode.install, "user_password": "yes", + }, + { + "name": "Simulator", + "status": "none", + "comment": "Simulator is a tool for managing Simulator devices.", + "install_text": "install", + "os": ["darwin"], + "status_function": simulator.check_status, + "install_function": simulator.install, + "user_password": "yes", + }, + { + "name": "WebDriver", + "status": "none", + "comment": "WebDriverAgent is required for iOS automation testing.", + "install_text": "install", + "os": ["darwin"], + "status_function": webdriver.check_status, + "install_function": webdriver.install, } ], }, From 161d95f18c03c4893ec3f1d3f464b01dbbbc35ca Mon Sep 17 00:00:00 2001 From: Mahmudul Alam Date: Sun, 7 Dec 2025 09:43:10 +0600 Subject: [PATCH 10/17] fix: Enhance iOS Simulator installation and status checking logic --- Framework/install_handler/ios/simulator.py | 108 +++++++++++++++++---- 1 file changed, 91 insertions(+), 17 deletions(-) diff --git a/Framework/install_handler/ios/simulator.py b/Framework/install_handler/ios/simulator.py index 93c92c8e6..a08fe9649 100644 --- a/Framework/install_handler/ios/simulator.py +++ b/Framework/install_handler/ios/simulator.py @@ -66,10 +66,16 @@ async def check_status() -> bool: if "iOS" in output or "iPhone" in output or "iPad" in output: # Count number of available devices device_lines = [line for line in output.splitlines() if "(" in line and ")" in line and "Booted" not in line] - await _send_status( - "installed", f"iOS Simulator available with {len(device_lines)} devices." - ) - return True + if len(device_lines) > 0: + await _send_status( + "installed", f"iOS Simulator available with {len(device_lines)} devices." + ) + return True + else: + await _send_status( + "not installed", "No iOS Simulator devices found. Install Xcode or simulator runtimes." + ) + return False else: await _send_status( "not installed", "No iOS Simulator devices found. Install Xcode or simulator runtimes." @@ -137,16 +143,26 @@ async def _install_command_line_tools(user_password: str) -> bool: return False -async def _install_simulator_runtime() -> bool: +async def _install_simulator_runtime(user_password: str) -> bool: """Install iOS Simulator runtime if missing.""" try: # First, ensure Xcode command line tools are set correctly - result = subprocess.run( - ["sudo", "xcode-select", "--switch", "/Applications/Xcode.app/Contents/Developer"], - capture_output=True, - text=True, - timeout=30, - ) + if user_password: + xcode_select_cmd = f"echo '{user_password}' | sudo -S xcode-select --switch /Applications/Xcode.app/Contents/Developer" + result = subprocess.run( + xcode_select_cmd, + shell=True, + capture_output=True, + text=True, + timeout=30, + ) + else: + result = subprocess.run( + ["xcode-select", "--switch", "/Applications/Xcode.app/Contents/Developer"], + capture_output=True, + text=True, + timeout=30, + ) # Check available simulator runtimes await _send_status( @@ -166,15 +182,73 @@ async def _install_simulator_runtime() -> bool: ) return False - # If no iOS runtimes found, guide user to install via Xcode - if "iOS" not in runtime_result.stdout: + # Check if iOS runtimes are present and have devices + has_ios_runtime = "iOS" in runtime_result.stdout + + if has_ios_runtime: + # Verify that there are actual simulator devices available + devices_result = subprocess.run( + ["xcrun", "simctl", "list", "devices", "available", "iOS"], + capture_output=True, + text=True, + timeout=30, + ) + + if devices_result.returncode == 0: + output = devices_result.stdout + device_lines = [line for line in output.splitlines() if "(" in line and ")" in line] + + if len(device_lines) > 0: + return True + + # No iOS runtimes or devices found, attempt to download and install + await _send_status( + "installing", + "Downloading iOS Simulator runtime, this may take several minutes..." + ) + + # Download iOS platform using xcodebuild + download_result = subprocess.run( + ["xcodebuild", "-downloadPlatform", "iOS"], + capture_output=True, + text=True, + timeout=3600, # 1 hour timeout for download + ) + + if download_result.returncode != 0: await _send_status( "error", - "No iOS runtimes found. Please install via Xcode > Settings > Platforms." + f"Failed to download platform. Please install via Xcode > Settings > Platforms. Error: {download_result.stderr.strip()[-200:]}" ) return False - - return True + + # Wait a moment for installation to complete + await asyncio.sleep(5) + + # Verify installation + verify_result = subprocess.run( + ["xcrun", "simctl", "list", "devices", "available", "iOS"], + capture_output=True, + text=True, + timeout=30, + ) + + if verify_result.returncode == 0: + output = verify_result.stdout + device_lines = [line for line in output.splitlines() if "(" in line and ")" in line] + + if len(device_lines) > 0: + await _send_status( + "installed", + f"iOS Simulator runtime installed successfully with {len(device_lines)} devices." + ) + return True + + await _send_status( + "error", + "Runtime installation completed but no devices found. Please verify via Xcode > Settings > Platforms." + ) + return False except Exception as e: await _send_status("error", f"Error installing simulator runtime: {e}") @@ -211,7 +285,7 @@ async def install(user_password: str = "") -> bool: return False # Install/verify simulator runtime - if not await _install_simulator_runtime(): + if not await _install_simulator_runtime(user_password): return False # Final status check From 8dbea980f14b5fa18d7a268f0cf2ae271863d21a Mon Sep 17 00:00:00 2001 From: Mahmudul Alam Date: Mon, 8 Dec 2025 06:20:59 +0600 Subject: [PATCH 11/17] Webdriver should be checked inside simulator --- Framework/install_handler/ios/webdriver.py | 132 +++++++++++++++++++-- 1 file changed, 119 insertions(+), 13 deletions(-) diff --git a/Framework/install_handler/ios/webdriver.py b/Framework/install_handler/ios/webdriver.py index b51d27de9..04f147d7f 100644 --- a/Framework/install_handler/ios/webdriver.py +++ b/Framework/install_handler/ios/webdriver.py @@ -48,8 +48,12 @@ async def _check_xcode_installed() -> bool: return False -async def _get_available_simulator() -> str | None: - """Get the first available iOS simulator device name.""" +async def _get_available_simulator() -> tuple[str, str] | None: + """Get the first available iOS simulator device name and UUID. + + Returns: + Tuple of (device_name, device_uuid) or None if not found. + """ try: result = subprocess.run( ["xcrun", "simctl", "list", "devices", "available", "iOS"], @@ -65,17 +69,65 @@ async def _get_available_simulator() -> str | None: for line in result.stdout.splitlines(): line = line.strip() # Look for device lines like "iPhone 16 Pro (UUID) (Shutdown)" - if "iPhone" in line and "(" in line: + if "iPhone" in line and "(" in line: # Extract device name (everything before first parenthesis) device_name = line.split("(")[0].strip() - if device_name: - return device_name + # Extract UUID (between first and second parenthesis) + parts = line.split("(") + if len(parts) >= 2: + uuid = parts[1].split(")")[0].strip() + if device_name and uuid: + return device_name, uuid return None except Exception: return None +async def _boot_simulator(device_uuid: str) -> bool: + """Boot the iOS simulator with the given UUID. + + Args: + device_uuid: The UUID of the simulator to boot. + + Returns: + True if successful, False otherwise. + """ + try: + # Check if already booted + result = subprocess.run( + ["xcrun", "simctl", "list", "devices"], + capture_output=True, + text=True, + timeout=30, + ) + + if result.returncode == 0 and device_uuid in result.stdout: + # Check if already booted + for line in result.stdout.splitlines(): + if device_uuid in line and "(Booted)" in line: + return True # Already booted + + # Boot the simulator + result = subprocess.run( + ["xcrun", "simctl", "boot", device_uuid], + capture_output=True, + text=True, + timeout=60, + ) + + if result.returncode != 0: + # Check if error is because it's already booted + if "Unable to boot device in current state: Booted" in result.stderr: + return True + return False + + return True + + except Exception: + return False + + async def check_status() -> bool: """Check if WebDriverAgent is installed and built.""" print("[webdriver] Checking status...") @@ -122,16 +174,57 @@ async def check_status() -> bool: return False # Check if WebDriverAgentRunner scheme exists - if "WebDriverAgentRunner" in result.stdout: - await _send_status( - "installed", f"WebDriverAgent is installed at {webdriver_path}" - ) - return True - else: + if "WebDriverAgentRunner" not in result.stdout: await _send_status( "not installed", "WebDriverAgentRunner scheme not found in project." ) return False + + # Check if WebDriverAgent is installed on any booted simulator + simulator_info = await _get_available_simulator() + if simulator_info: + simulator_name, simulator_uuid = simulator_info + + # Check if simulator is booted + list_result = subprocess.run( + ["xcrun", "simctl", "list", "devices"], + capture_output=True, + text=True, + timeout=30, + ) + + is_booted = False + if list_result.returncode == 0: + for line in list_result.stdout.splitlines(): + if simulator_uuid in line and "(Booted)" in line: + is_booted = True + break + + if is_booted: + # Check if WebDriverAgentRunner is installed on the booted simulator + app_check = subprocess.run( + ["xcrun", "simctl", "get_app_container", simulator_uuid, "com.facebook.WebDriverAgentRunner.xctrunner"], + capture_output=True, + text=True, + timeout=30, + ) + + if app_check.returncode == 0 and app_check.stdout.strip(): + await _send_status( + "installed", f"WebDriverAgent is installed on {simulator_name}" + ) + return True + else: + await _send_status( + "not installed", f"WebDriverAgent is built but not installed on {simulator_name}" + ) + return False + + # If no simulator is booted, just verify the project is valid + await _send_status( + "installed", f"WebDriverAgent is built at {webdriver_path} (no simulator booted to verify installation)" + ) + return True except subprocess.TimeoutExpired: await _send_status("error", "xcodebuild command timed out.") @@ -228,12 +321,25 @@ async def _build_webdriver(webdriver_path: Path) -> bool: project_path = webdriver_path / "WebDriverAgent.xcodeproj" # Get available simulator - simulator_name = await _get_available_simulator() - if not simulator_name: + simulator_info = await _get_available_simulator() + if not simulator_info: await _send_status( "error", "No iOS Simulator found. Please install iOS Simulator first." ) return False + + simulator_name, simulator_uuid = simulator_info + + # Boot the simulator first + await _send_status( + "installing", f"Booting {simulator_name} simulator..." + ) + + if not await _boot_simulator(simulator_uuid): + await _send_status( + "error", f"Failed to boot {simulator_name} simulator." + ) + return False await _send_status( "installing", f"Building WebDriverAgent for {simulator_name}, please wait (this may take several minutes)..." From 52f2fc984728e970637c30d78202bd0312b100b7 Mon Sep 17 00:00:00 2001 From: Mahmudul Alam Date: Mon, 8 Dec 2025 09:06:53 +0600 Subject: [PATCH 12/17] successful installation of simulator and webdriver --- Framework/install_handler/ios/simulator.py | 339 ++++++-------- Framework/install_handler/ios/webdriver.py | 493 ++++++--------------- 2 files changed, 289 insertions(+), 543 deletions(-) diff --git a/Framework/install_handler/ios/simulator.py b/Framework/install_handler/ios/simulator.py index a08fe9649..fdee43a48 100644 --- a/Framework/install_handler/ios/simulator.py +++ b/Framework/install_handler/ios/simulator.py @@ -3,9 +3,10 @@ import os import shutil import subprocess +import json +import re from Framework.install_handler.utils import send_response - async def _send_status(status: str, comment: str): """Helper to send status responses.""" await send_response( @@ -20,209 +21,187 @@ async def _send_status(status: str, comment: str): } ) +async def _create_default_device() -> bool: + """ + Attempts to create a default iPhone simulator using the newest available + iOS runtime and the newest available iPhone device type. + """ + await _send_status("installing", "No devices found. Creating a new default simulator...") + + try: + # 1. Get available Runtimes (JSON is safer to parse) + # We need to find the installed iOS runtime ID (e.g. com.apple.CoreSimulator.SimRuntime.iOS-18-0) + runtime_proc = subprocess.run( + ["xcrun", "simctl", "list", "runtimes", "-j"], + capture_output=True, + text=True + ) + if runtime_proc.returncode != 0: + raise Exception("Failed to list runtimes.") + + runtimes_data = json.loads(runtime_proc.stdout) + available_runtimes = runtimes_data.get("runtimes", []) + + # Filter for iOS runtimes and sort to find the latest version + ios_runtimes = [ + r for r in available_runtimes + if r.get("platform") == "iOS" or "iOS" in r.get("name", "") + ] + + if not ios_runtimes: + raise Exception("No iOS Runtimes found after installation.") + + # Sort by version (assuming name contains version or using buildversion) + # We'll just pick the first one which is usually the latest in the list, + # or we can try to sort by the 'version' key if available. + target_runtime = ios_runtimes[-1] # Usually the list is ordered, taking the last one is a safe bet for 'newest' + runtime_id = target_runtime["identifier"] + runtime_name = target_runtime["name"] + + # 2. Get available Device Types + type_proc = subprocess.run( + ["xcrun", "simctl", "list", "devicetypes", "-j"], + capture_output=True, + text=True + ) + types_data = json.loads(type_proc.stdout) + + # Find a modern iPhone (e.g., iPhone 16, 15, or just the last "iPhone" in the list) + device_types = [ + t for t in types_data.get("devicetypes", []) + if "iPhone" in t.get("name", "") + ] + + if not device_types: + raise Exception("No iPhone device types found.") + + # Pick the last one (usually the newest model) + target_device_type = device_types[-1] + device_type_id = target_device_type["identifier"] + device_name = target_device_type["name"] + + # 3. Create the device + new_device_name = f"{device_name} (Default)" + await _send_status("installing", f"Creating {new_device_name} with {runtime_name}...") + + create_proc = subprocess.run( + ["xcrun", "simctl", "create", new_device_name, device_type_id, runtime_id], + capture_output=True, + text=True + ) + + if create_proc.returncode == 0: + new_udid = create_proc.stdout.strip() + await _send_status("installed", f"Created new device: {new_device_name} ({new_udid})") + return True + else: + raise Exception(f"Failed to create device: {create_proc.stderr}") + + except Exception as e: + await _send_status("error", f"Auto-creation of device failed: {e}") + return False async def check_status() -> bool: """Check if iOS Simulator is installed and available.""" print("[simulator] Checking status...") if platform.system().lower() != "darwin": - await _send_status( - "error", "Unsupported OS. iOS Simulator is only available on macOS." - ) + await _send_status("error", "Unsupported OS. iOS Simulator is only available on macOS.") return False - # Check if Xcode is installed first (required for simulators) if not os.path.exists("/Applications/Xcode.app"): - await _send_status( - "not installed", "Xcode must be installed before using iOS Simulator." - ) + await _send_status("not installed", "Xcode must be installed before using iOS Simulator.") return False - # Check if simctl is available - simctl_path = shutil.which("xcrun") - if not simctl_path: - await _send_status( - "not installed", "xcrun not found. Xcode command line tools may not be installed." - ) + if not shutil.which("xcrun"): + await _send_status("not installed", "xcrun not found. Xcode command line tools missing.") return False try: - # Check if any simulators are available + # Check for available devices result = subprocess.run( - ["xcrun", "simctl", "list", "devices", "available"], + ["xcrun", "simctl", "list", "devices", "available", "iOS"], capture_output=True, text=True, timeout=30, ) if result.returncode != 0: - await _send_status( - "error", f"simctl error: {result.stderr.strip()}" - ) + await _send_status("error", f"simctl error: {result.stderr.strip()}") return False - # Check if there are any iOS simulators available output = result.stdout - if "iOS" in output or "iPhone" in output or "iPad" in output: - # Count number of available devices - device_lines = [line for line in output.splitlines() if "(" in line and ")" in line and "Booted" not in line] - if len(device_lines) > 0: - await _send_status( - "installed", f"iOS Simulator available with {len(device_lines)} devices." - ) - return True - else: - await _send_status( - "not installed", "No iOS Simulator devices found. Install Xcode or simulator runtimes." - ) - return False + # Basic check: verify lines with UDIDs exist + device_lines = [line for line in output.splitlines() if "(" in line and ")" in line] + + if len(device_lines) > 0: + await _send_status("installed", f"iOS Simulator available with {len(device_lines)} devices.") + return True else: - await _send_status( - "not installed", "No iOS Simulator devices found. Install Xcode or simulator runtimes." - ) + await _send_status("not installed", "No iOS Simulator devices found.") return False - except subprocess.TimeoutExpired: - await _send_status("error", "simctl command timed out.") - return False except Exception as e: await _send_status("error", f"Error checking iOS Simulator: {e}") return False - -async def _install_command_line_tools(user_password: str) -> bool: +async def _install_command_line_tools() -> bool: """Install Xcode command line tools if not present.""" try: - # Check if already installed - result = subprocess.run( - ["xcode-select", "-p"], - capture_output=True, - text=True, - timeout=10, - ) - + result = subprocess.run(["xcode-select", "-p"], capture_output=True, text=True) if result.returncode == 0: return True - # Install command line tools - await _send_status( - "installing", "Installing Xcode command line tools, please wait..." - ) - - install_result = subprocess.run( - ["xcode-select", "--install"], - capture_output=True, - text=True, - timeout=10, - ) + await _send_status("installing", "Installing Xcode command line tools...") + subprocess.run(["xcode-select", "--install"], capture_output=True, text=True) - # Wait for installation to complete (poll for up to 30 minutes) - timeout_seconds = 30 * 60 - interval = 10 - elapsed = 0 - - while elapsed < timeout_seconds: - await asyncio.sleep(interval) - elapsed += interval - - check_result = subprocess.run( - ["xcode-select", "-p"], - capture_output=True, - text=True, - timeout=10, - ) - - if check_result.returncode == 0: + # Poll for completion (simplified for brevity) + for _ in range(60): # wait up to 10 mins + await asyncio.sleep(10) + if subprocess.run(["xcode-select", "-p"], capture_output=True).returncode == 0: return True - - await _send_status("error", "Timed out waiting for command line tools installation.") + + await _send_status("error", "Timed out waiting for command line tools.") return False - except Exception as e: - await _send_status("error", f"Error installing command line tools: {e}") + await _send_status("error", f"Error installing tools: {e}") return False - async def _install_simulator_runtime(user_password: str) -> bool: """Install iOS Simulator runtime if missing.""" try: - # First, ensure Xcode command line tools are set correctly + # Ensure xcode-select is pointing to Xcode app + cmd = ["xcode-select", "--switch", "/Applications/Xcode.app/Contents/Developer"] if user_password: - xcode_select_cmd = f"echo '{user_password}' | sudo -S xcode-select --switch /Applications/Xcode.app/Contents/Developer" - result = subprocess.run( - xcode_select_cmd, - shell=True, - capture_output=True, - text=True, - timeout=30, + # Use sudo -S for password piping if provided + subprocess.run( + f"echo '{user_password}' | sudo -S {' '.join(cmd)}", + shell=True, capture_output=True ) else: - result = subprocess.run( - ["xcode-select", "--switch", "/Applications/Xcode.app/Contents/Developer"], - capture_output=True, - text=True, - timeout=30, - ) + subprocess.run(cmd, capture_output=True) - # Check available simulator runtimes - await _send_status( - "installing", "Checking for available iOS Simulator runtimes..." - ) + # Check existing runtimes + await _send_status("installing", "Checking iOS Simulator runtimes...") - runtime_result = subprocess.run( - ["xcrun", "simctl", "list", "runtimes"], - capture_output=True, - text=True, - timeout=30, - ) + # Download iOS platform using xcodebuild (Your provided block) + await _send_status("installing", "Downloading iOS Simulator runtime (this may take a while)...") - if runtime_result.returncode != 0: - await _send_status( - "error", f"Failed to list runtimes: {runtime_result.stderr.strip()}" - ) - return False - - # Check if iOS runtimes are present and have devices - has_ios_runtime = "iOS" in runtime_result.stdout - - if has_ios_runtime: - # Verify that there are actual simulator devices available - devices_result = subprocess.run( - ["xcrun", "simctl", "list", "devices", "available", "iOS"], - capture_output=True, - text=True, - timeout=30, - ) - - if devices_result.returncode == 0: - output = devices_result.stdout - device_lines = [line for line in output.splitlines() if "(" in line and ")" in line] - - if len(device_lines) > 0: - return True - - # No iOS runtimes or devices found, attempt to download and install - await _send_status( - "installing", - "Downloading iOS Simulator runtime, this may take several minutes..." - ) - - # Download iOS platform using xcodebuild - download_result = subprocess.run( + with subprocess.Popen( ["xcodebuild", "-downloadPlatform", "iOS"], - capture_output=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, - timeout=3600, # 1 hour timeout for download - ) - - if download_result.returncode != 0: - await _send_status( - "error", - f"Failed to download platform. Please install via Xcode > Settings > Platforms. Error: {download_result.stderr.strip()[-200:]}" - ) - return False + bufsize=1 + ) as process: + for line in process.stdout: + await _send_status("installing", line.strip()) + + if process.returncode != 0: + # Only fail if it's a real error. Sometimes it fails if already installed. + # We proceed to verification regardless. + pass - # Wait a moment for installation to complete await asyncio.sleep(5) # Verify installation @@ -233,69 +212,45 @@ async def _install_simulator_runtime(user_password: str) -> bool: timeout=30, ) + device_count = 0 if verify_result.returncode == 0: - output = verify_result.stdout - device_lines = [line for line in output.splitlines() if "(" in line and ")" in line] - - if len(device_lines) > 0: - await _send_status( - "installed", - f"iOS Simulator runtime installed successfully with {len(device_lines)} devices." - ) - return True - - await _send_status( - "error", - "Runtime installation completed but no devices found. Please verify via Xcode > Settings > Platforms." - ) - return False + device_lines = [line for line in verify_result.stdout.splitlines() if "(" in line and ")" in line] + device_count = len(device_lines) + + # LOGIC CHANGE: If installed but no devices, create one. + if device_count > 0: + await _send_status("installed", f"iOS Simulator runtime ready with {device_count} devices.") + return True + else: + # Runtime might be there, but no devices created yet. + # Attempt to create a default device. + return await _create_default_device() except Exception as e: await _send_status("error", f"Error installing simulator runtime: {e}") return False - async def install(user_password: str = "") -> bool: - """Install iOS Simulator (requires Xcode to be installed first).""" + """Main install entry point.""" print("[simulator] Installing...") if platform.system().lower() != "darwin": - await _send_status( - "error", "Unsupported OS. iOS Simulator is only available on macOS." - ) + await _send_status("error", "iOS Simulator is only available on macOS.") return False - # Check if already installed and working if await check_status(): return True - # Ensure Xcode is installed if not os.path.exists("/Applications/Xcode.app"): - await _send_status( - "error", "Xcode must be installed first. Please install Xcode from the App Store." - ) + await _send_status("error", "Xcode must be installed first.") return False - await _send_status( - "installing", "Setting up iOS Simulator..." - ) + await _send_status("installing", "Setting up iOS Simulator...") - # Install command line tools if needed - if not await _install_command_line_tools(user_password): + if not await _install_command_line_tools(): return False - # Install/verify simulator runtime if not await _install_simulator_runtime(user_password): return False - # Final status check - if await check_status(): - await _send_status( - "installed", "iOS Simulator is ready to use." - ) - return True - else: - await _send_status( - "error", "iOS Simulator setup completed but verification failed." - ) - return False + return await check_status() \ No newline at end of file diff --git a/Framework/install_handler/ios/webdriver.py b/Framework/install_handler/ios/webdriver.py index 04f147d7f..9e1ac95b9 100644 --- a/Framework/install_handler/ios/webdriver.py +++ b/Framework/install_handler/ios/webdriver.py @@ -2,12 +2,14 @@ import os import shutil import subprocess +import json +import tempfile from pathlib import Path from Framework.install_handler.utils import send_response - async def _send_status(status: str, comment: str): """Helper to send status responses.""" + print(f"[{status}] {comment}") await send_response( { "action": "status", @@ -20,425 +22,214 @@ async def _send_status(status: str, comment: str): } ) - def _get_webdriver_path() -> Path: - """Get the path where WebDriverAgent should be installed.""" home = Path.home() return home / ".zeuz" / "WebDriverAgent" - async def _check_xcode_installed() -> bool: - """Check if Xcode is installed.""" if not os.path.exists("/Applications/Xcode.app"): return False - - xcodebuild_path = shutil.which("xcodebuild") - if not xcodebuild_path: - return False - - try: - result = subprocess.run( - [xcodebuild_path, "-version"], - capture_output=True, - text=True, - timeout=30, - ) - return result.returncode == 0 - except Exception: - return False - + return shutil.which("xcodebuild") is not None -async def _get_available_simulator() -> tuple[str, str] | None: - """Get the first available iOS simulator device name and UUID. - - Returns: - Tuple of (device_name, device_uuid) or None if not found. - """ +async def _get_best_simulator() -> tuple[str, str] | None: + """Finds the best candidate simulator (preferring iPhones).""" try: result = subprocess.run( - ["xcrun", "simctl", "list", "devices", "available", "iOS"], - capture_output=True, - text=True, - timeout=30, + ["xcrun", "simctl", "list", "devices", "available", "-j"], + capture_output=True, text=True ) if result.returncode != 0: return None - # Parse output to find first iPhone or iPad simulator - for line in result.stdout.splitlines(): - line = line.strip() - # Look for device lines like "iPhone 16 Pro (UUID) (Shutdown)" - if "iPhone" in line and "(" in line: - # Extract device name (everything before first parenthesis) - device_name = line.split("(")[0].strip() - # Extract UUID (between first and second parenthesis) - parts = line.split("(") - if len(parts) >= 2: - uuid = parts[1].split(")")[0].strip() - if device_name and uuid: - return device_name, uuid - + data = json.loads(result.stdout) + candidates = [] + + for runtime_key, devices in data.get("devices", {}).items(): + if "iOS" not in runtime_key: + continue + for device in devices: + if device.get("isAvailable"): + name = device.get("name") + uuid = device.get("udid") + score = 2 if "iPhone" in name else 1 + candidates.append((score, name, uuid)) + + candidates.sort(key=lambda x: x[0], reverse=True) + if candidates: + return candidates[0][1], candidates[0][2] return None - except Exception: + except Exception as e: + print(f"Error listing simulators: {e}") return None - -async def _boot_simulator(device_uuid: str) -> bool: - """Boot the iOS simulator with the given UUID. - - Args: - device_uuid: The UUID of the simulator to boot. - - Returns: - True if successful, False otherwise. - """ - try: - # Check if already booted - result = subprocess.run( - ["xcrun", "simctl", "list", "devices"], - capture_output=True, - text=True, - timeout=30, - ) - - if result.returncode == 0 and device_uuid in result.stdout: - # Check if already booted - for line in result.stdout.splitlines(): - if device_uuid in line and "(Booted)" in line: - return True # Already booted - - # Boot the simulator - result = subprocess.run( - ["xcrun", "simctl", "boot", device_uuid], - capture_output=True, - text=True, - timeout=60, - ) - - if result.returncode != 0: - # Check if error is because it's already booted - if "Unable to boot device in current state: Booted" in result.stderr: - return True - return False - - return True - - except Exception: - return False - - async def check_status() -> bool: - """Check if WebDriverAgent is installed and built.""" - print("[webdriver] Checking status...") + """Checks if WebDriverAgent is installed (Passive Check).""" + print("[webdriver] Checking status (passive)...") if platform.system().lower() != "darwin": - await _send_status( - "error", "Unsupported OS. WebDriverAgent is only available on macOS." - ) + await _send_status("error", "Unsupported OS.") return False - # Check if Xcode is installed if not await _check_xcode_installed(): - await _send_status( - "not installed", "Xcode must be installed before using WebDriverAgent." - ) + await _send_status("not installed", "Xcode missing.") return False webdriver_path = _get_webdriver_path() - project_path = webdriver_path / "WebDriverAgent.xcodeproj" - - # Check if WebDriverAgent is cloned - if not project_path.exists(): - await _send_status( - "not installed", "WebDriverAgent repository is not cloned." - ) + if not (webdriver_path / "WebDriverAgent.xcodeproj").exists(): + await _send_status("not installed", "Repo not cloned.") return False - # Check if the project has been built - # Look for derived data or built products - try: - # Check if we can read the project - result = subprocess.run( - ["xcodebuild", "-project", str(project_path), "-list"], - capture_output=True, - text=True, - timeout=30, - cwd=str(webdriver_path), - ) + sim_info = await _get_best_simulator() + if not sim_info: + await _send_status("not installed", "No iOS Simulator devices found.") + return False - if result.returncode != 0: - await _send_status( - "not installed", f"WebDriverAgent project is invalid: {result.stderr.strip()}" - ) - return False + name, uuid = sim_info - # Check if WebDriverAgentRunner scheme exists - if "WebDriverAgentRunner" not in result.stdout: - await _send_status( - "not installed", "WebDriverAgentRunner scheme not found in project." - ) - return False - - # Check if WebDriverAgent is installed on any booted simulator - simulator_info = await _get_available_simulator() - if simulator_info: - simulator_name, simulator_uuid = simulator_info - - # Check if simulator is booted - list_result = subprocess.run( - ["xcrun", "simctl", "list", "devices"], - capture_output=True, - text=True, - timeout=30, - ) - - is_booted = False - if list_result.returncode == 0: - for line in list_result.stdout.splitlines(): - if simulator_uuid in line and "(Booted)" in line: - is_booted = True - break - - if is_booted: - # Check if WebDriverAgentRunner is installed on the booted simulator - app_check = subprocess.run( - ["xcrun", "simctl", "get_app_container", simulator_uuid, "com.facebook.WebDriverAgentRunner.xctrunner"], - capture_output=True, - text=True, - timeout=30, - ) - - if app_check.returncode == 0 and app_check.stdout.strip(): - await _send_status( - "installed", f"WebDriverAgent is installed on {simulator_name}" - ) - return True - else: - await _send_status( - "not installed", f"WebDriverAgent is built but not installed on {simulator_name}" - ) - return False - - # If no simulator is booted, just verify the project is valid - await _send_status( - "installed", f"WebDriverAgent is built at {webdriver_path} (no simulator booted to verify installation)" - ) + # Check if app container exists on the device's disk + cmd = ["xcrun", "simctl", "get_app_container", uuid, "com.facebook.WebDriverAgentRunner.xctrunner"] + check_res = subprocess.run(cmd, capture_output=True, text=True) + + if check_res.returncode == 0 and check_res.stdout.strip(): + await _send_status("installed", f"WebDriverAgent found on {name} ({uuid}).") return True - - except subprocess.TimeoutExpired: - await _send_status("error", "xcodebuild command timed out.") - return False - except Exception as e: - await _send_status("error", f"Error checking WebDriverAgent: {e}") + else: + await _send_status("not installed", f"WebDriverAgent not found on {name} ({uuid}).") return False - -async def _clone_repository(webdriver_path: Path) -> bool: - """Clone the WebDriverAgent repository.""" +async def _boot_simulator_if_needed(device_uuid: str) -> bool: + """Boots the simulator ONLY if it is currently shutdown.""" try: - # Remove existing directory if it exists but is incomplete - if webdriver_path.exists(): - await _send_status( - "installing", "Removing incomplete WebDriverAgent installation..." - ) - shutil.rmtree(webdriver_path) - - # Create parent directory - webdriver_path.parent.mkdir(parents=True, exist_ok=True) - - await _send_status( - "installing", "Cloning WebDriverAgent repository, please wait..." - ) + list_res = subprocess.run( + ["xcrun", "simctl", "list", "devices", "-j"], + capture_output=True, text=True + ) + if list_res.returncode == 0: + data = json.loads(list_res.stdout) + for runtime, devices in data.get("devices", {}).items(): + for device in devices: + if device.get("udid") == device_uuid: + if device.get("state") == "Booted": + await _send_status("installing", "Simulator is already booted.") + return True + + await _send_status("installing", "Booting simulator for installation...") + subprocess.run(["open", "-a", "Simulator"], capture_output=True) + subprocess.run(["xcrun", "simctl", "boot", device_uuid], capture_output=True) + + res = subprocess.run( + ["xcrun", "simctl", "bootstatus", device_uuid], + capture_output=True, timeout=120 + ) + return res.returncode == 0 + except Exception: + return True - # Clone the repository - result = subprocess.run( - [ - "git", "clone", - "--depth", "1", # Shallow clone for faster download - "https://github.com/appium/WebDriverAgent.git", - str(webdriver_path) - ], - capture_output=True, - text=True, - timeout=600, # 10 minutes timeout - ) +async def _build_and_install_webdriver(webdriver_path: Path) -> bool: + try: + sim_info = await _get_best_simulator() + if not sim_info: return False + name, uuid = sim_info - if result.returncode != 0: - await _send_status( - "error", f"Failed to clone repository: {result.stderr.strip()}" - ) + if not await _boot_simulator_if_needed(uuid): + await _send_status("error", "Failed to boot simulator.") return False - return True - - except subprocess.TimeoutExpired: - await _send_status("error", "Git clone timed out.") - return False - except Exception as e: - await _send_status("error", f"Error cloning repository: {e}") - return False + await _send_status("installing", f"Building WebDriverAgent for {name}...") - -async def _bootstrap_webdriver(webdriver_path: Path) -> bool: - """Run the bootstrap script if it exists.""" - bootstrap_script = webdriver_path / "Scripts" / "bootstrap.sh" - - if not bootstrap_script.exists(): - # Try alternative path - bootstrap_script = webdriver_path / "bootstrap.sh" - - if bootstrap_script.exists(): - try: - await _send_status( - "installing", "Running WebDriverAgent bootstrap script..." - ) + # Create a temp directory to store build artifacts so we can find the .app later + with tempfile.TemporaryDirectory() as derived_data_path: + cmd = [ + "xcodebuild", + "-project", "WebDriverAgent.xcodeproj", + "-scheme", "WebDriverAgentRunner", + "-destination", f"platform=iOS Simulator,id={uuid}", + "-derivedDataPath", derived_data_path, + "-allowProvisioningUpdates", + "build-for-testing" + ] result = subprocess.run( - ["bash", str(bootstrap_script)], - capture_output=True, - text=True, - timeout=600, - cwd=str(webdriver_path), + cmd, cwd=str(webdriver_path), + capture_output=True, text=True, timeout=1200 ) - - if result.returncode != 0: - # Non-fatal, just log it - print(f"Bootstrap warning: {result.stderr.strip()}") - - return True - except Exception as e: - # Non-fatal - print(f"Bootstrap warning: {e}") - return True - - return True + if result.returncode != 0: + err = result.stderr[-500:] if result.stderr else "Unknown error" + await _send_status("error", f"Build failed: {err}") + return False -async def _build_webdriver(webdriver_path: Path) -> bool: - """Build WebDriverAgent project.""" - try: - project_path = webdriver_path / "WebDriverAgent.xcodeproj" - - # Get available simulator - simulator_info = await _get_available_simulator() - if not simulator_info: - await _send_status( - "error", "No iOS Simulator found. Please install iOS Simulator first." + # Explicitly Install the Built App + await _send_status("installing", "Installing WebDriverAgentRunner-Runner.app...") + + # The standard path inside derived data for the simulator build + app_path = Path(derived_data_path) / "Build" / "Products" / "Debug-iphonesimulator" / "WebDriverAgentRunner-Runner.app" + + if not app_path.exists(): + await _send_status("error", f"Could not find built app at {app_path}") + return False + + install_res = subprocess.run( + ["xcrun", "simctl", "install", uuid, str(app_path)], + capture_output=True, text=True ) - return False - - simulator_name, simulator_uuid = simulator_info + + if install_res.returncode != 0: + await _send_status("error", f"Install failed: {install_res.stderr.strip()}") + return False - # Boot the simulator first - await _send_status( - "installing", f"Booting {simulator_name} simulator..." - ) - - if not await _boot_simulator(simulator_uuid): - await _send_status( - "error", f"Failed to boot {simulator_name} simulator." - ) - return False + await _send_status("installing", "App installed successfully via simctl.") + return True - await _send_status( - "installing", f"Building WebDriverAgent for {simulator_name}, please wait (this may take several minutes)..." - ) + except Exception as e: + await _send_status("error", f"Build/Install exception: {e}") + return False - # Build the project - destination = f"platform=iOS Simulator,name={simulator_name}" +async def _clone_repository(webdriver_path: Path) -> bool: + try: + if webdriver_path.exists(): + shutil.rmtree(webdriver_path) + webdriver_path.parent.mkdir(parents=True, exist_ok=True) - result = subprocess.run( - [ - "xcodebuild", - "-project", str(project_path), - "-scheme", "WebDriverAgentRunner", - "-destination", destination, - "-allowProvisioningUpdates", - "build-for-testing", - ], - capture_output=True, - text=True, - timeout=1800, # 30 minutes timeout for build - cwd=str(webdriver_path), - ) - - if result.returncode != 0: - # Check if it's a signing error (common issue) - if "code signing" in result.stderr.lower() or "signing" in result.stderr.lower(): - await _send_status( - "error", - "Code signing error. Please open Xcode, go to WebDriverAgent project, " - "and configure signing in the target settings." - ) - else: - await _send_status( - "error", f"Build failed: {result.stderr.strip()[-500:]}" # Last 500 chars - ) - return False - - await _send_status( - "installed", f"WebDriverAgent built successfully for {simulator_name}" + await _send_status("installing", "Cloning WebDriverAgent...") + subprocess.run( + ["git", "clone", "--depth", "1", "https://github.com/appium/WebDriverAgent.git", str(webdriver_path)], + check=True, capture_output=True ) return True - - except subprocess.TimeoutExpired: - await _send_status("error", "Build timed out (exceeded 30 minutes).") - return False except Exception as e: - await _send_status("error", f"Error building WebDriverAgent: {e}") + await _send_status("error", f"Clone failed: {e}") return False +async def _bootstrap_webdriver(webdriver_path: Path): + script = webdriver_path / "Scripts" / "bootstrap.sh" + if script.exists(): + try: + await _send_status("installing", "Running bootstrap...") + subprocess.run(["bash", str(script)], cwd=str(webdriver_path), capture_output=True) + except: pass async def install(user_password: str = "") -> bool: - """Install WebDriverAgent by cloning and building the project.""" - print("[webdriver] Installing...") - - if platform.system().lower() != "darwin": - await _send_status( - "error", "Unsupported OS. WebDriverAgent is only available on macOS." - ) - return False - - # Check if already installed + print("[webdriver] Starting installation...") + if await check_status(): return True - # Ensure Xcode is installed if not await _check_xcode_installed(): - await _send_status( - "error", "Xcode must be installed first. Please install Xcode from the App Store." - ) - return False - - # Check if git is available - if not shutil.which("git"): - await _send_status( - "error", "Git is not installed. Please install git first." - ) + await _send_status("error", "Xcode required.") return False webdriver_path = _get_webdriver_path() - # Clone repository - if not await _clone_repository(webdriver_path): - return False - - # Bootstrap (optional step) + if not await _clone_repository(webdriver_path): return False await _bootstrap_webdriver(webdriver_path) - # Build the project - if not await _build_webdriver(webdriver_path): - return False + if not await _build_and_install_webdriver(webdriver_path): return False - # Final verification if await check_status(): - await _send_status( - "installed", f"WebDriverAgent is ready to use at {webdriver_path}" - ) + await _send_status("installed", "WebDriverAgent installed successfully.") return True else: - await _send_status( - "error", "Installation completed but verification failed." - ) + await _send_status("error", "Installation completed but verification failed.") return False From b906ed9229a2c3a58a02d408c10bba7c8faa914c Mon Sep 17 00:00:00 2001 From: Mahmudul Alam Date: Tue, 9 Dec 2025 04:51:04 +0600 Subject: [PATCH 13/17] start simulator before checking status webdriver --- Framework/install_handler/ios/webdriver.py | 83 +++++++++++++--------- 1 file changed, 49 insertions(+), 34 deletions(-) diff --git a/Framework/install_handler/ios/webdriver.py b/Framework/install_handler/ios/webdriver.py index 9e1ac95b9..abef0d64b 100644 --- a/Framework/install_handler/ios/webdriver.py +++ b/Framework/install_handler/ios/webdriver.py @@ -4,6 +4,7 @@ import subprocess import json import tempfile +import asyncio from pathlib import Path from Framework.install_handler.utils import send_response @@ -63,9 +64,47 @@ async def _get_best_simulator() -> tuple[str, str] | None: print(f"Error listing simulators: {e}") return None +async def _boot_simulator_if_needed(device_uuid: str) -> bool: + """Boots the simulator ONLY if it is currently shutdown.""" + try: + # 1. Check current state + list_res = subprocess.run( + ["xcrun", "simctl", "list", "devices", "-j"], + capture_output=True, text=True + ) + if list_res.returncode == 0: + data = json.loads(list_res.stdout) + for runtime, devices in data.get("devices", {}).items(): + for device in devices: + if device.get("udid") == device_uuid: + if device.get("state") == "Booted": + # Already booted + return True + + # 2. Boot if needed + await _send_status("installing", "Booting simulator to perform check/install...") + subprocess.run(["open", "-a", "Simulator"], capture_output=True) + + # Give the app a moment to launch + await asyncio.sleep(2) + + subprocess.run(["xcrun", "simctl", "boot", device_uuid], capture_output=True) + + # 3. Wait for 'Booted' status + res = subprocess.run( + ["xcrun", "simctl", "bootstatus", device_uuid], + capture_output=True, timeout=120 + ) + return res.returncode == 0 + except Exception as e: + print(f"Boot exception: {e}") + # We return True here to attempt the next step anyway, + # as sometimes bootstatus fails even if the device works. + return True + async def check_status() -> bool: - """Checks if WebDriverAgent is installed (Passive Check).""" - print("[webdriver] Checking status (passive)...") + """Checks if WebDriverAgent is installed (Ensures Simulator is ON).""" + print("[webdriver] Checking status...") if platform.system().lower() != "darwin": await _send_status("error", "Unsupported OS.") @@ -87,7 +126,11 @@ async def check_status() -> bool: name, uuid = sim_info - # Check if app container exists on the device's disk + if not await _boot_simulator_if_needed(uuid): + await _send_status("error", f"Failed to boot {name} for verification.") + return False + + # Check if app container exists cmd = ["xcrun", "simctl", "get_app_container", uuid, "com.facebook.WebDriverAgentRunner.xctrunner"] check_res = subprocess.run(cmd, capture_output=True, text=True) @@ -98,47 +141,20 @@ async def check_status() -> bool: await _send_status("not installed", f"WebDriverAgent not found on {name} ({uuid}).") return False -async def _boot_simulator_if_needed(device_uuid: str) -> bool: - """Boots the simulator ONLY if it is currently shutdown.""" - try: - list_res = subprocess.run( - ["xcrun", "simctl", "list", "devices", "-j"], - capture_output=True, text=True - ) - if list_res.returncode == 0: - data = json.loads(list_res.stdout) - for runtime, devices in data.get("devices", {}).items(): - for device in devices: - if device.get("udid") == device_uuid: - if device.get("state") == "Booted": - await _send_status("installing", "Simulator is already booted.") - return True - - await _send_status("installing", "Booting simulator for installation...") - subprocess.run(["open", "-a", "Simulator"], capture_output=True) - subprocess.run(["xcrun", "simctl", "boot", device_uuid], capture_output=True) - - res = subprocess.run( - ["xcrun", "simctl", "bootstatus", device_uuid], - capture_output=True, timeout=120 - ) - return res.returncode == 0 - except Exception: - return True - async def _build_and_install_webdriver(webdriver_path: Path) -> bool: try: sim_info = await _get_best_simulator() if not sim_info: return False name, uuid = sim_info + # Ensure booted (check_status might have done this, but good to double check) if not await _boot_simulator_if_needed(uuid): await _send_status("error", "Failed to boot simulator.") return False await _send_status("installing", f"Building WebDriverAgent for {name}...") - # Create a temp directory to store build artifacts so we can find the .app later + # Create a temp directory to store build artifacts with tempfile.TemporaryDirectory() as derived_data_path: cmd = [ "xcodebuild", @@ -163,7 +179,6 @@ async def _build_and_install_webdriver(webdriver_path: Path) -> bool: # Explicitly Install the Built App await _send_status("installing", "Installing WebDriverAgentRunner-Runner.app...") - # The standard path inside derived data for the simulator build app_path = Path(derived_data_path) / "Build" / "Products" / "Debug-iphonesimulator" / "WebDriverAgentRunner-Runner.app" if not app_path.exists(): @@ -232,4 +247,4 @@ async def install(user_password: str = "") -> bool: return True else: await _send_status("error", "Installation completed but verification failed.") - return False + return False \ No newline at end of file From 54146be5e116df0aa356c11e9b086c37901d8e4d Mon Sep 17 00:00:00 2001 From: Mahmudul Alam Date: Tue, 9 Dec 2025 08:42:37 +0600 Subject: [PATCH 14/17] added group for v2 --- Framework/install_handler/route.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Framework/install_handler/route.py b/Framework/install_handler/route.py index 7adfa8f39..a4fc7c0db 100644 --- a/Framework/install_handler/route.py +++ b/Framework/install_handler/route.py @@ -147,6 +147,10 @@ }, { "category": "iOS", + "group": { + "check_text": "check all", + "install_text": "install all", + }, "services": [ { "name": "Xcode", @@ -181,6 +185,10 @@ }, { "category": "MacOS", + "group": { + "check_text": "", + "install_text": "", + }, "services": [ { "name": "Xcode", From feed67981d0d94630e76dd052dff0ace5aa76e5a Mon Sep 17 00:00:00 2001 From: Mahmudul Alam Date: Tue, 9 Dec 2025 08:44:41 +0600 Subject: [PATCH 15/17] removed password requirements --- Framework/install_handler/ios/webdriver.py | 2 +- Framework/install_handler/route.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Framework/install_handler/ios/webdriver.py b/Framework/install_handler/ios/webdriver.py index abef0d64b..6e5d0801e 100644 --- a/Framework/install_handler/ios/webdriver.py +++ b/Framework/install_handler/ios/webdriver.py @@ -225,7 +225,7 @@ async def _bootstrap_webdriver(webdriver_path: Path): subprocess.run(["bash", str(script)], cwd=str(webdriver_path), capture_output=True) except: pass -async def install(user_password: str = "") -> bool: +async def install() -> bool: print("[webdriver] Starting installation...") if await check_status(): diff --git a/Framework/install_handler/route.py b/Framework/install_handler/route.py index a4fc7c0db..a1834c786 100644 --- a/Framework/install_handler/route.py +++ b/Framework/install_handler/route.py @@ -180,6 +180,7 @@ "os": ["darwin"], "status_function": webdriver.check_status, "install_function": webdriver.install, + "user_password": "no", } ], }, From fb54cb4c6f7e8cdf3740d7426ab65e3750f537b7 Mon Sep 17 00:00:00 2001 From: Mahmudul Alam Date: Wed, 10 Dec 2025 06:33:07 +0600 Subject: [PATCH 16/17] added emulator installation support --- Framework/install_handler/ios/simulator.py | 796 ++++++++++++++++++ .../install_handler/long_poll_handler.py | 45 +- Framework/install_handler/route.py | 33 +- 3 files changed, 854 insertions(+), 20 deletions(-) diff --git a/Framework/install_handler/ios/simulator.py b/Framework/install_handler/ios/simulator.py index fdee43a48..73dad5681 100644 --- a/Framework/install_handler/ios/simulator.py +++ b/Framework/install_handler/ios/simulator.py @@ -5,6 +5,8 @@ import subprocess import json import re +import tempfile +from pathlib import Path from Framework.install_handler.utils import send_response async def _send_status(status: str, comment: str): @@ -230,6 +232,800 @@ async def _install_simulator_runtime(user_password: str) -> bool: await _send_status("error", f"Error installing simulator runtime: {e}") return False +async def get_available_simulators() -> list[dict]: + """ + List available iOS Simulators by running xcrun simctl list devices. + Returns a list of dictionaries with name, udid, and state fields. + """ + try: + if platform.system().lower() != "darwin": + return [] + + if not shutil.which("xcrun"): + return [] + + result = subprocess.run( + ["xcrun", "simctl", "list", "devices", "available", "-j"], + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode != 0: + return [] + + data = json.loads(result.stdout) + simulators = [] + + for runtime_key, devices in data.get("devices", {}).items(): + if "iOS" not in runtime_key: + continue + + # Extract iOS version from runtime key (e.g., "com.apple.CoreSimulator.SimRuntime.iOS-18-2" -> "iOS 18.2") + version_match = re.search(r'iOS-(\d+)-(\d+)', runtime_key) + runtime_version = f"iOS {version_match.group(1)}.{version_match.group(2)}" if version_match else runtime_key + + for device in devices: + if device.get("isAvailable"): + simulators.append({ + "name": device.get("name"), + "udid": device.get("udid"), + "state": device.get("state"), + "runtime": runtime_version, + "comment": f"{device.get('name')} - {runtime_version} ({device.get('state')})" + }) + + return simulators + + except Exception as e: + print(f"[simulator] Error listing simulators: {e}") + return [] + + +async def get_available_device_types() -> list[dict]: + """ + Get available iOS device types by running xcrun simctl list devicetypes. + Returns a list of dictionaries with package (device type ID), version (device name), and description. + Format matches Android's device list for consistency. + """ + try: + if platform.system().lower() != "darwin": + return [] + + if not shutil.which("xcrun"): + return [] + + result = subprocess.run( + ["xcrun", "simctl", "list", "devicetypes", "-j"], + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode != 0: + return [] + + data = json.loads(result.stdout) + device_types = [] + + for device_type in data.get("devicetypes", []): + name = device_type.get("name", "") + identifier = device_type.get("identifier", "") + + # Filter for iPhone and iPad devices only + if "iPhone" in name or "iPad" in name: + device_types.append({ + "package": identifier, # device type ID + "version": name, # device name + "description": "Apple" # OEM + }) + + return device_types + + except Exception as e: + print(f"[simulator] Error listing device types: {e}") + return [] + + +async def get_available_runtimes() -> list[dict]: + """ + Get available iOS runtimes by running xcrun simctl list runtimes. + Returns a list of dictionaries with runtime ID, version, and availability. + """ + try: + if platform.system().lower() != "darwin": + return [] + + if not shutil.which("xcrun"): + return [] + + result = subprocess.run( + ["xcrun", "simctl", "list", "runtimes", "-j"], + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode != 0: + return [] + + data = json.loads(result.stdout) + runtimes = [] + + for runtime in data.get("runtimes", []): + platform_name = runtime.get("platform", "") + name = runtime.get("name", "") + + # Filter for iOS runtimes only + if platform_name == "iOS" or "iOS" in name: + runtimes.append({ + "identifier": runtime.get("identifier", ""), + "name": name, + "version": runtime.get("version", ""), + "isAvailable": runtime.get("isAvailable", False) + }) + + return runtimes + + except Exception as e: + print(f"[simulator] Error listing runtimes: {e}") + return [] + + +async def ios_simulator_install(): + """ + Get available device types when install button is clicked. + Returns list of available iOS device types for simulator creation. + """ + print("[simulator] Getting available device types...") + + try: + # Check if macOS + if platform.system().lower() != "darwin": + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": "Device Types", + "status": "Not Found", + "comment": "iOS Simulator is only available on macOS", + "installables": [] + } + }) + return False + + # Check if Xcode is installed + if not os.path.exists("/Applications/Xcode.app"): + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": "Device Types", + "status": "Not Found", + "comment": "Xcode must be installed first", + "installables": [] + } + }) + return False + + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": "Device Types", + "status": "Fetching", + "comment": "Fetching available device types...", + "installables": [] + } + }) + + # Get available device types + device_types = await get_available_device_types() + print(f"[simulator] Available device types: {device_types}") + + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "installables": device_types, + } + }) + return True + + except Exception as e: + print(f"[simulator] Error getting device types: {e}") + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": "Device Types", + "status": "error", + "comment": f"Error: {e}", + "installables": [] + } + }) + return False + + +async def get_filtered_simulator_services(): + """ + Get available iOS simulators and return a formatted category dictionary. + Similar to Android's get_filtered_avd_services. + + Returns: + Dictionary with category "iOSSimulator" and filtered services, or None if no simulators + """ + current_os = platform.system().lower() + + # iOS simulators only available on macOS + if current_os != "darwin": + return None + + try: + simulators = await get_available_simulators() + + if not simulators: + return None + + # Format simulators as services + services_list = [] + for sim in simulators: + services_list.append({ + "name": sim["udid"], # Use UDID as the identifier + "status": "installed", + "comment": sim["comment"], + "install_text": "delete", + "install_function": "delete_simulator", + "os": ["darwin"], + "user_password": "no", + }) + + return { + "group": { + "check_text": "", + "install_text": "", + }, + "category": "iOSSimulator", + "services": services_list, + } + + except Exception as e: + print(f"[simulator] Error getting filtered simulators: {e}") + return None + + +async def delete_simulator(udid: str) -> bool: + """ + Delete iOS Simulator by UDID using simctl delete. + Sends response to server on success or failure. + + Args: + udid: The UDID of the simulator to delete + + Returns: + bool: True if successful, False otherwise + """ + try: + # Get simulator info first to show proper name in messages + simulators = await get_available_simulators() + simulator_info = next((s for s in simulators if s["udid"] == udid), None) + + if not simulator_info: + error_msg = f"Simulator {udid} not found" + print(f"[simulator] {error_msg}") + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": udid, + "status": "not installed", + "comment": error_msg, + } + }) + return False + + simulator_name = simulator_info["name"] + print(f"[simulator] Deleting simulator: {simulator_name} ({udid})") + + # Send deleting status + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": udid, + "status": "uninstalling", + "comment": f"Deleting simulator {simulator_name}...", + } + }) + + # Delete the simulator + result = subprocess.run( + ["xcrun", "simctl", "delete", udid], + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode != 0: + error_msg = f"Failed to delete simulator: {result.stderr.strip()}" + print(f"[simulator] {error_msg}") + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": udid, + "status": "error", + "comment": error_msg, + } + }) + return False + + print(f"[simulator] Simulator deleted successfully: {simulator_name} ({udid})") + + # Send success response + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": udid, + "status": "not installed", + "comment": f"Simulator '{simulator_name}' deleted successfully", + } + }) + return True + + except subprocess.TimeoutExpired: + error_msg = "Simulator deletion timed out" + print(f"[simulator] {error_msg}") + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": udid, + "status": "error", + "comment": error_msg, + } + }) + return False + except Exception as e: + error_msg = f"Error deleting simulator: {e}" + print(f"[simulator] {error_msg}") + import traceback + traceback.print_exc() + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": udid, + "status": "error", + "comment": error_msg, + } + }) + return False + + +async def launch_simulator(udid: str) -> bool: + """ + Launch iOS Simulator using simctl boot and open Simulator app. + Checks for WebDriverAgent installation and installs if needed. + Launches WebDriverAgent app automatically. + Non-blocking - the simulator starts in the background. + Sends response to server on success or failure. + """ + try: + # Get simulator info first + simulators = await get_available_simulators() + simulator_info = next((s for s in simulators if s["udid"] == udid), None) + + if not simulator_info: + error_msg = f"Simulator {udid} not found" + print(f"[simulator] {error_msg}") + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": udid, + "status": "not installed", + "comment": error_msg, + } + }) + return False + + simulator_name = simulator_info['name'] + is_already_booted = simulator_info["state"] == "Booted" + + # Boot simulator if not already booted + if not is_already_booted: + # Open Simulator app + subprocess.Popen( + ["open", "-a", "Simulator"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + + # Small delay to let Simulator app launch + await asyncio.sleep(1) + + # Boot the specific device + result = subprocess.run( + ["xcrun", "simctl", "boot", udid], + capture_output=True, + text=True + ) + + if result.returncode != 0: + error_msg = f"Failed to boot simulator: {result.stderr.strip()}" + print(f"[simulator] {error_msg}") + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": udid, + "status": "error", + "comment": error_msg, + } + }) + return False + + print(f"[simulator] Booting simulator: {simulator_name}...") + + # Wait for boot to complete + await asyncio.sleep(3) + else: + print(f"[simulator] Simulator {simulator_name} already running") + + # Check if WebDriverAgent is installed on this simulator + print(f"[simulator] Checking WebDriverAgent installation on {simulator_name}...") + check_wda = subprocess.run( + ["xcrun", "simctl", "get_app_container", udid, "com.facebook.WebDriverAgentRunner.xctrunner"], + capture_output=True, + text=True + ) + + wda_installed = check_wda.returncode == 0 and check_wda.stdout.strip() + + if not wda_installed: + print(f"[simulator] WebDriverAgent not found on {simulator_name}, installing...") + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": udid, + "status": "installing", + "comment": f"Installing WebDriverAgent on {simulator_name}...", + } + }) + + # Get WebDriverAgent path (same as webdriver.py uses) + home = Path.home() + webdriver_path = home / ".zeuz" / "WebDriverAgent" + + # Check if WebDriverAgent repo exists + if not (webdriver_path / "WebDriverAgent.xcodeproj").exists(): + print(f"[simulator] Cloning WebDriverAgent repository...") + if webdriver_path.exists(): + shutil.rmtree(webdriver_path) + webdriver_path.parent.mkdir(parents=True, exist_ok=True) + + clone_result = subprocess.run( + ["git", "clone", "--depth", "1", "https://github.com/appium/WebDriverAgent.git", str(webdriver_path)], + capture_output=True, + text=True + ) + + if clone_result.returncode != 0: + error_msg = f"Failed to clone WebDriverAgent: {clone_result.stderr}" + print(f"[simulator] {error_msg}") + # Continue with launching simulator even if WDA install fails + + # Build and install WebDriverAgent for this simulator + if (webdriver_path / "WebDriverAgent.xcodeproj").exists(): + # Check if app is already built (in standard location) + standard_build_path = webdriver_path / "Build" / "Products" / "Debug-iphonesimulator" / "WebDriverAgentRunner-Runner.app" + app_path_to_install = None + + if standard_build_path.exists(): + print(f"[simulator] Found pre-built WebDriverAgent at {standard_build_path}") + app_path_to_install = standard_build_path + else: + # Need to build + print(f"[simulator] Building WebDriverAgent for {simulator_name}...") + + with tempfile.TemporaryDirectory() as derived_data_path: + build_cmd = [ + "xcodebuild", + "-project", "WebDriverAgent.xcodeproj", + "-scheme", "WebDriverAgentRunner", + "-destination", f"platform=iOS Simulator,id={udid}", + "-derivedDataPath", derived_data_path, + "-allowProvisioningUpdates", + "build-for-testing" + ] + + build_result = subprocess.run( + build_cmd, + cwd=str(webdriver_path), + capture_output=True, + text=True, + timeout=1200 + ) + + if build_result.returncode == 0: + app_path = Path(derived_data_path) / "Build" / "Products" / "Debug-iphonesimulator" / "WebDriverAgentRunner-Runner.app" + + if app_path.exists(): + app_path_to_install = app_path + else: + print(f"[simulator] WebDriverAgent app not found at {app_path}") + else: + print(f"[simulator] WebDriverAgent build failed: {build_result.stderr[-500:]}") + + # Install the app if we have it + if app_path_to_install: + print(f"[simulator] Installing WebDriverAgent on {simulator_name}...") + install_result = subprocess.run( + ["xcrun", "simctl", "install", udid, str(app_path_to_install)], + capture_output=True, + text=True + ) + + if install_result.returncode == 0: + print(f"[simulator] WebDriverAgent installed successfully on {simulator_name}") + wda_installed = True + else: + print(f"[simulator] Failed to install WebDriverAgent: {install_result.stderr}") + else: + print(f"[simulator] WebDriverAgent already installed on {simulator_name}") + + # Launch WebDriverAgent if installed + if wda_installed: + print(f"[simulator] Launching WebDriverAgent on {simulator_name}...") + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": udid, + "status": "installing", + "comment": f"Launching WebDriverAgent on {simulator_name}...", + } + }) + + # Launch WebDriverAgent app + launch_wda = subprocess.run( + ["xcrun", "simctl", "launch", udid, "com.facebook.WebDriverAgentRunner.xctrunner"], + capture_output=True, + text=True + ) + + if launch_wda.returncode == 0: + print(f"[simulator] WebDriverAgent launched successfully on {simulator_name}") + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": udid, + "status": "installed", + "comment": f"Simulator {simulator_name} running with WebDriverAgent", + } + }) + else: + print(f"[simulator] Failed to launch WebDriverAgent: {launch_wda.stderr}") + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": udid, + "status": "installed", + "comment": f"Simulator {simulator_name} running (WebDriverAgent launch failed)", + } + }) + else: + # Simulator is running but WDA not available + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": udid, + "status": "installed", + "comment": f"Simulator {simulator_name} is running", + } + }) + + return True + + except subprocess.TimeoutExpired: + error_msg = "Operation timed out" + print(f"[simulator] {error_msg}") + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": udid, + "status": "error", + "comment": error_msg, + } + }) + return False + except Exception as e: + error_msg = f"Failed to launch simulator: {e}" + print(f"[simulator] {error_msg}") + import traceback + traceback.print_exc() + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": udid, + "status": "error", + "comment": error_msg, + } + }) + return False + + +async def create_simulator_from_device_type(device_param: str) -> bool: + """ + Create iOS Simulator from device type ID and device name. + Uses the highest available iOS runtime. + + Args: + device_param: Format "install device;device_type_id;device_name" + Example: "install device;com.apple.CoreSimulator.SimDeviceType.iPhone-16;iPhone 16" + + Returns: + bool: True if successful, False otherwise + """ + try: + # Parse device parameter + parts = device_param.split(";") + if len(parts) < 3: + error_msg = f"Invalid device parameter format. Expected 'install device;device_type_id;device_name', got: {device_param}" + print(f"[simulator] {error_msg}") + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": "Unknown", + "status": "error", + "comment": error_msg, + } + }) + return False + + device_type_id = parts[1].strip() + device_name = parts[2].strip() + + print(f"[simulator] Creating simulator '{device_name}' with device type '{device_type_id}'") + + # Get available runtimes + runtimes = await get_available_runtimes() + if not runtimes: + error_msg = "No iOS runtimes found. Please install Xcode and iOS runtime first." + print(f"[simulator] {error_msg}") + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": device_name, + "status": "not installed", + "comment": error_msg, + } + }) + return False + + # Get the latest available runtime + available_runtimes = [r for r in runtimes if r.get("isAvailable", False)] + if not available_runtimes: + error_msg = "No available iOS runtimes found." + print(f"[simulator] {error_msg}") + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": device_name, + "status": "not installed", + "comment": error_msg, + } + }) + return False + + # Use the last runtime (usually the newest) + runtime = available_runtimes[-1] + runtime_id = runtime["identifier"] + runtime_name = runtime["name"] + + print(f"[simulator] Using runtime: {runtime_name} ({runtime_id})") + + # Generate a unique simulator name + existing_sims = await get_available_simulators() + existing_names = [s["name"] for s in existing_sims] + + # Create unique name + simulator_name = device_name + counter = 1 + while simulator_name in existing_names: + simulator_name = f"{device_name} ({counter})" + counter += 1 + + # Send installing status + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": device_name, + "status": "installing", + "comment": f"Creating {simulator_name} with {runtime_name}...", + } + }) + + # Create the simulator + result = subprocess.run( + ["xcrun", "simctl", "create", simulator_name, device_type_id, runtime_id], + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode != 0: + error_msg = f"Failed to create simulator: {result.stderr.strip()}" + print(f"[simulator] {error_msg}") + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": device_name, + "status": "not installed", + "comment": error_msg, + } + }) + return False + + new_udid = result.stdout.strip() + print(f"[simulator] Simulator created successfully: {simulator_name} ({new_udid})") + + # Send success response + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": device_name, + "status": "installed", + "comment": f"Simulator '{simulator_name}' created successfully ({new_udid})", + } + }) + + return True + + except subprocess.TimeoutExpired: + error_msg = "Simulator creation timed out" + print(f"[simulator] {error_msg}") + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": device_name if 'device_name' in locals() else "Unknown", + "status": "error", + "comment": error_msg, + } + }) + return False + except Exception as e: + error_msg = f"Error creating simulator: {e}" + print(f"[simulator] {error_msg}") + import traceback + traceback.print_exc() + await send_response({ + "action": "status", + "data": { + "category": "iOSSimulator", + "name": device_name if 'device_name' in locals() else "Unknown", + "status": "error", + "comment": error_msg, + } + }) + return False + + async def install(user_password: str = "") -> bool: """Main install entry point.""" print("[simulator] Installing...") diff --git a/Framework/install_handler/long_poll_handler.py b/Framework/install_handler/long_poll_handler.py index 8eb5d02eb..d136826b8 100644 --- a/Framework/install_handler/long_poll_handler.py +++ b/Framework/install_handler/long_poll_handler.py @@ -3,13 +3,13 @@ import random import httpx import inspect -import platform from colorama import Fore from Framework.install_handler.route import Response, services from Framework.install_handler.utils import debug, send_response, read_node_id, generate_services_list from Framework.Utilities import RequestFormatter, ConfigModule from Framework.node_server_state import STATE from Framework.install_handler.android.emulator import create_avd_from_system_image, get_filtered_avd_services, get_available_avds, launch_avd +from Framework.install_handler.ios.simulator import create_simulator_from_device_type, get_filtered_simulator_services, launch_simulator from Framework.install_handler.system_info.system_info import get_formatted_system_info if debug: @@ -31,10 +31,17 @@ async def on_message(self, message: Response) -> None: if action == "services_list": services_list = generate_services_list(services) + # Add Android AVD list avd_list = await get_filtered_avd_services() if avd_list: services_list.insert(1, avd_list) + # Add iOS Simulator list (insert after AVD list if present, or at index 1) + simulator_list = await get_filtered_simulator_services() + if simulator_list: + insert_index = 2 if avd_list else 1 + services_list.insert(insert_index, simulator_list) + await send_response({ "action": "services_list", "data": { @@ -103,6 +110,42 @@ async def on_message(self, message: Response) -> None: return return + # Handle iOSSimulator category + if category["category"] == "iOSSimulator": + print(f"[installer] iOSSimulator category: {category}") + print(f"[installer] iOSSimulator services: {category['services']}") + print(f"[installer] Requested service name: {message.value.item.name}") + service_name = message.value.item.name + + # Case 1: No service name or empty - get device types list + if not service_name: + if action == "install" and "install_function" in category and category["install_function"]: + func = category["install_function"] + await func() + return + else: + print(f"[installer] No install_function found for iOSSimulator category") + return + + # Case 2: Service name is a device type (starts with "install device;") + if service_name.startswith("install device;"): + if action == "install": + await create_simulator_from_device_type(service_name) + return + else: + print(f"[installer] Status check not supported for device types") + return + + # Case 3: This is a request to launch an existing simulator (UDID format) + else: + try: + await launch_simulator(service_name) + except Exception as e: + print(f"[installer] Error launching simulator '{service_name}': {e}") + traceback.print_exc() + return + return + # Normal service-level install for other categories service = [i for i in category["services"] if i["name"] == message.value.item.name][0] if action == "install": diff --git a/Framework/install_handler/route.py b/Framework/install_handler/route.py index a1834c786..56496af38 100644 --- a/Framework/install_handler/route.py +++ b/Framework/install_handler/route.py @@ -1,6 +1,5 @@ from pydantic import BaseModel, ConfigDict from typing import Literal, Optional -import platform from .web import chrome_for_testing, edge, mozilla from .android import ( @@ -8,21 +7,15 @@ node_js_22, appium, java, - android_emulator, android_sdk, jdk, - emulator, ) -from .ios import xcode, simulator, webdriver +from .ios import xcode, simulator from .macos import xcode as macos_xcode from .database import postgresql, mysql, mariadb, oracle from .windows import inspector from .android.emulator import android_emulator_install - -import httpx -from Framework.Utilities import RequestFormatter, ConfigModule, CommonUtil -import datetime -from Framework.install_handler.utils import debug +from .ios.simulator import ios_simulator_install services = [ { @@ -106,6 +99,18 @@ "installables": [], "services": [], }, + { + "group": { + "check_text": "", + "install_text": "", + }, + "category": "iOSSimulator", + "name": "Device Types", + "install_text": "install", + "install_function": ios_simulator_install, + "installables": [], + "services": [], + }, { "category": "Web", "group": { @@ -172,16 +177,6 @@ "install_function": simulator.install, "user_password": "yes", }, - { - "name": "WebDriver", - "status": "none", - "comment": "WebDriverAgent is required for iOS automation testing.", - "install_text": "install", - "os": ["darwin"], - "status_function": webdriver.check_status, - "install_function": webdriver.install, - "user_password": "no", - } ], }, { From b0717c126f7091175871882ac43a93e9fe35519a Mon Sep 17 00:00:00 2001 From: Mahmudul Alam Date: Wed, 10 Dec 2025 06:40:46 +0600 Subject: [PATCH 17/17] copy built app before deleting --- Framework/install_handler/ios/simulator.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Framework/install_handler/ios/simulator.py b/Framework/install_handler/ios/simulator.py index 73dad5681..593d71548 100644 --- a/Framework/install_handler/ios/simulator.py +++ b/Framework/install_handler/ios/simulator.py @@ -754,7 +754,11 @@ async def launch_simulator(udid: str) -> bool: app_path = Path(derived_data_path) / "Build" / "Products" / "Debug-iphonesimulator" / "WebDriverAgentRunner-Runner.app" if app_path.exists(): - app_path_to_install = app_path + # Copy to standard location before temp directory is deleted + standard_build_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(app_path, standard_build_path, dirs_exist_ok=True) + print(f"[simulator] Copied built app to {standard_build_path}") + app_path_to_install = standard_build_path else: print(f"[simulator] WebDriverAgent app not found at {app_path}") else: