From 652d702634270cbac195df3d0da2071e37c659ee Mon Sep 17 00:00:00 2001 From: mdshakib007 Date: Mon, 19 Jan 2026 10:28:03 +0600 Subject: [PATCH 1/4] ios app installation done --- server/mobile.py | 132 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 1 deletion(-) diff --git a/server/mobile.py b/server/mobile.py index 1fea8cc9..532d0213 100644 --- a/server/mobile.py +++ b/server/mobile.py @@ -6,10 +6,12 @@ import subprocess import base64 import json -from typing import Literal +from typing import Literal, Optional import asyncio import socket import xml.etree.ElementTree as ET +import zipfile +import plistlib import requests from androguard.core.apk import APK @@ -533,3 +535,131 @@ def handle_apk_install(filename: str, serial: str): except Exception as e: return {"message": f"Error installing APK: {str(e)}", "filename": filename, "package_name": package_name} + +@router.get("/package-installed") +def check_package_installed(package_name: str, serial: str | None = None): + """Check if an android package is installed on the device.""" + try: + device_flag = f"-s {serial}" if serial else "" + result = run_adb_command( + f"{ADB_PATH} {device_flag} shell pm list packages {package_name}".strip() + ) + if result.startswith("Error:"): + return {"installed": False, "package_name": package_name, "error": result} + + # pm list packages returns lines like "package:com.example.app" + # Check if the exact package is in the output + installed_packages = [line.replace("package:", "") for line in result.split("\n") if line.startswith("package:")] + is_installed = package_name in installed_packages + + return {"installed": is_installed, "package_name": package_name} + except Exception as e: + return {"installed": False, "package_name": package_name, "error": str(e)} + + +@router.post("/ios/app-upload") +def handle_ios_app_upload(file: UploadFile = File(...)): + dir_path = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app" + if not os.path.exists(dir_path): + os.makedirs(dir_path) + + filename = file.filename or "uploaded.app" + filepath = os.path.join(dir_path, filename) + with open(filepath, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + return {"message": "App uploaded successfully", "filename": filename} + + +def normalized_ios_app_path(file_path: str) -> Optional[str]: + """ + ensure that we ended up with a .app directory even if user provided .ipa + """ + if not os.path.exists(file_path): + return None + + # if already .app + if file_path.endswith(".app") and os.path.isdir(file_path): + return file_path + + # .ipa, so we have to extract to get .app + if file_path.endswith(".ipa"): + extract_dir = file_path.replace(".ipa", "_extracted") + zip_path = file_path.replace(".ipa", ".zip") + + # copy instead of rename to avoid breaking original file + shutil.copy(file_path, zip_path) + + with zipfile.ZipFile(zip_path, "r") as zip_ref: + zip_ref.extractall(extract_dir) + + payload_dir = os.path.join(extract_dir, "Payload") + if not os.path.isdir(payload_dir): + return None + + for item in os.listdir(payload_dir): + if item.endswith(".app"): + return os.path.join(payload_dir, item) + + return None + +def extract_bundle_id_from_app(app_path: str) -> Optional[str]: + """ + Reads CFBundleIdentifier from Info.plist inside .app bundle + """ + info_plist_path = os.path.join(app_path, "Info.plist") + + if not os.path.exists(info_plist_path): + return None + + try: + with open(info_plist_path, "rb") as f: + plist_data = plistlib.load(f) + return plist_data.get("CFBundleIdentifier") + except Exception: + return None + + +@router.post("/ios/app-install") +def handle_ios_app_install(filename: str, sim_udid: str): + """ + handling the ios app installation in the simolator + """ + dirpath = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app" + filepath = os.path.join(dirpath, filename) + if not os.path.exists(dirpath): + return {"message": "App not found", "filename": filename} + + app_path = normalized_ios_app_path(filepath) + if not app_path: + return {"message": "Failed to normalize .app", "filename": filename} + + bundle_id = extract_bundle_id_from_app(app_path) + + try: + subprocess.run( + ["xcrun", "simctl", "install", sim_udid, app_path], + check=True + ) + return { + "message": "App installed successfully", + "filename": filename, + "bundle_id": bundle_id, + } + except subprocess.CalledProcessError as e: + return { + "message": f"Error installing app: {str(e)}", + "filename": filename, + "bundle_id": bundle_id, + } + + +@router.get("/ios/bundle-installed") +def is_ios_app_installed(sim_udid: str, bundle_id: str) -> bool: + result = subprocess.run( + ["xcrun", "simctl", "get_app_container", sim_udid, bundle_id], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + return result.returncode == 0 \ No newline at end of file From 142d38c0e8f9ade0d8d61f1c77c889bbb5459ba0 Mon Sep 17 00:00:00 2001 From: shakib Date: Mon, 19 Jan 2026 10:54:52 +0600 Subject: [PATCH 2/4] ios app installation check code updated --- server/mobile.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/server/mobile.py b/server/mobile.py index 532d0213..f3cb7907 100644 --- a/server/mobile.py +++ b/server/mobile.py @@ -655,11 +655,14 @@ def handle_ios_app_install(filename: str, sim_udid: str): @router.get("/ios/bundle-installed") -def is_ios_app_installed(sim_udid: str, bundle_id: str) -> bool: - result = subprocess.run( - ["xcrun", "simctl", "get_app_container", sim_udid, bundle_id], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - return result.returncode == 0 \ No newline at end of file +def is_ios_app_installed(sim_udid: str, bundle_id: str): + try: + result = subprocess.run( + ["xcrun", "simctl", "get_app_container", sim_udid, bundle_id], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + return {"installed": result.returncode == 0} + except Exception as e: + return {"installed": False, "error": str(e)} \ No newline at end of file From 3721bf52ace0cb7d112d2b3d7f2d56ba2e2954db Mon Sep 17 00:00:00 2001 From: mdshakib007 Date: Mon, 19 Jan 2026 12:09:11 +0600 Subject: [PATCH 3/4] uninstall first before installing an ios app --- server/mobile.py | 50 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/server/mobile.py b/server/mobile.py index f3cb7907..eaa855dc 100644 --- a/server/mobile.py +++ b/server/mobile.py @@ -627,7 +627,7 @@ def handle_ios_app_install(filename: str, sim_udid: str): """ dirpath = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app" filepath = os.path.join(dirpath, filename) - if not os.path.exists(dirpath): + if not os.path.exists(filepath): return {"message": "App not found", "filename": filename} app_path = normalized_ios_app_path(filepath) @@ -637,10 +637,34 @@ def handle_ios_app_install(filename: str, sim_udid: str): bundle_id = extract_bundle_id_from_app(app_path) try: - subprocess.run( + # uninstall if already exists + try: + subprocess.run( + ["xcrun", "simctl", "uninstall", sim_udid, bundle_id], + capture_output=True, text=True, timeout=30 + ) + except: + pass # Ignore uninstall errors + + # Install the app + result = subprocess.run( ["xcrun", "simctl", "install", sim_udid, app_path], - check=True + capture_output=True, text=True, check=True, timeout=120 ) + + # Verify installation + verify_result = subprocess.run( + ["xcrun", "simctl", "get_app_container", sim_udid, bundle_id], + capture_output=True, text=True, timeout=30 + ) + + if verify_result.returncode != 0: + return { + "message": f"App installed but verification failed: {verify_result.stderr}", + "filename": filename, + "bundle_id": bundle_id, + } + return { "message": "App installed successfully", "filename": filename, @@ -648,7 +672,7 @@ def handle_ios_app_install(filename: str, sim_udid: str): } except subprocess.CalledProcessError as e: return { - "message": f"Error installing app: {str(e)}", + "message": f"Error installing app: {e.stderr or str(e)}", "filename": filename, "bundle_id": bundle_id, } @@ -659,10 +683,20 @@ def is_ios_app_installed(sim_udid: str, bundle_id: str): try: result = subprocess.run( ["xcrun", "simctl", "get_app_container", sim_udid, bundle_id], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, + capture_output=True, text=True, timeout=10 ) - return {"installed": result.returncode == 0} + + if result.returncode == 0 and result.stdout.strip(): + list_result = subprocess.run( + ["xcrun", "simctl", "listapps", sim_udid], + capture_output=True, text=True, timeout=10 + ) + + if list_result.returncode == 0: + return {"installed": bundle_id in list_result.stdout} + + return {"installed": True} + + return {"installed": False} except Exception as e: return {"installed": False, "error": str(e)} \ No newline at end of file From 5e5cc7548f3a0b42c132111b5fce02b786290921 Mon Sep 17 00:00:00 2001 From: mdshakib007 Date: Mon, 19 Jan 2026 22:00:03 +0600 Subject: [PATCH 4/4] for ios app install: .zip handled now --- server/mobile.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/server/mobile.py b/server/mobile.py index eaa855dc..478b38b2 100644 --- a/server/mobile.py +++ b/server/mobile.py @@ -406,9 +406,6 @@ def capture_ios_ui_dump(device_udid: str): return except: pass - - # No real source available - raise Exception("iOS service error. Make sure simulator is running.") async def upload_android_ui_dump(): @@ -602,6 +599,25 @@ def normalized_ios_app_path(file_path: str) -> Optional[str]: return os.path.join(payload_dir, item) return None + + # extract zip because 'test.app' is not a file, so browser zip that before send to me + if file_path.endswith(".app.zip") or file_path.endswith(".zip"): + extract_dir = file_path + "_extracted" + os.makedirs(extract_dir, exist_ok=True) + + with zipfile.ZipFile(file_path, "r") as zip_ref: + zip_ref.extractall(extract_dir) + + # search recursively for .app folder so it will pick very first .app folder + for root, dirs, files in os.walk(extract_dir): + for d in dirs: + if d.endswith(".app") and os.path.isdir(os.path.join(root, d)): + return os.path.join(root, d) + + return None + + return None + def extract_bundle_id_from_app(app_path: str) -> Optional[str]: """