Skip to content

Commit 760153e

Browse files
mdshakib007mdshakib007
andauthored
iOS app installation (#651)
* ios app installation done * ios app installation check code updated * uninstall first before installing an ios app * for ios app install: .zip handled now --------- Co-authored-by: mdshakib007 <shakib@mail.com>
1 parent 2cec567 commit 760153e

1 file changed

Lines changed: 187 additions & 4 deletions

File tree

server/mobile.py

Lines changed: 187 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
import subprocess
77
import base64
88
import json
9-
from typing import Literal
9+
from typing import Literal, Optional
1010
import asyncio
1111
import socket
1212
import xml.etree.ElementTree as ET
13+
import zipfile
14+
import plistlib
1315

1416
import requests
1517
from androguard.core.apk import APK
@@ -404,9 +406,6 @@ def capture_ios_ui_dump(device_udid: str):
404406
return
405407
except:
406408
pass
407-
408-
# No real source available
409-
raise Exception("iOS service error. Make sure simulator is running.")
410409

411410

412411
async def upload_android_ui_dump():
@@ -533,3 +532,187 @@ def handle_apk_install(filename: str, serial: str):
533532
except Exception as e:
534533
return {"message": f"Error installing APK: {str(e)}", "filename": filename, "package_name": package_name}
535534

535+
536+
@router.get("/package-installed")
537+
def check_package_installed(package_name: str, serial: str | None = None):
538+
"""Check if an android package is installed on the device."""
539+
try:
540+
device_flag = f"-s {serial}" if serial else ""
541+
result = run_adb_command(
542+
f"{ADB_PATH} {device_flag} shell pm list packages {package_name}".strip()
543+
)
544+
if result.startswith("Error:"):
545+
return {"installed": False, "package_name": package_name, "error": result}
546+
547+
# pm list packages returns lines like "package:com.example.app"
548+
# Check if the exact package is in the output
549+
installed_packages = [line.replace("package:", "") for line in result.split("\n") if line.startswith("package:")]
550+
is_installed = package_name in installed_packages
551+
552+
return {"installed": is_installed, "package_name": package_name}
553+
except Exception as e:
554+
return {"installed": False, "package_name": package_name, "error": str(e)}
555+
556+
557+
@router.post("/ios/app-upload")
558+
def handle_ios_app_upload(file: UploadFile = File(...)):
559+
dir_path = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app"
560+
if not os.path.exists(dir_path):
561+
os.makedirs(dir_path)
562+
563+
filename = file.filename or "uploaded.app"
564+
filepath = os.path.join(dir_path, filename)
565+
with open(filepath, "wb") as buffer:
566+
shutil.copyfileobj(file.file, buffer)
567+
568+
return {"message": "App uploaded successfully", "filename": filename}
569+
570+
571+
def normalized_ios_app_path(file_path: str) -> Optional[str]:
572+
"""
573+
ensure that we ended up with a .app directory even if user provided .ipa
574+
"""
575+
if not os.path.exists(file_path):
576+
return None
577+
578+
# if already .app
579+
if file_path.endswith(".app") and os.path.isdir(file_path):
580+
return file_path
581+
582+
# .ipa, so we have to extract to get .app
583+
if file_path.endswith(".ipa"):
584+
extract_dir = file_path.replace(".ipa", "_extracted")
585+
zip_path = file_path.replace(".ipa", ".zip")
586+
587+
# copy instead of rename to avoid breaking original file
588+
shutil.copy(file_path, zip_path)
589+
590+
with zipfile.ZipFile(zip_path, "r") as zip_ref:
591+
zip_ref.extractall(extract_dir)
592+
593+
payload_dir = os.path.join(extract_dir, "Payload")
594+
if not os.path.isdir(payload_dir):
595+
return None
596+
597+
for item in os.listdir(payload_dir):
598+
if item.endswith(".app"):
599+
return os.path.join(payload_dir, item)
600+
601+
return None
602+
603+
# extract zip because 'test.app' is not a file, so browser zip that before send to me
604+
if file_path.endswith(".app.zip") or file_path.endswith(".zip"):
605+
extract_dir = file_path + "_extracted"
606+
os.makedirs(extract_dir, exist_ok=True)
607+
608+
with zipfile.ZipFile(file_path, "r") as zip_ref:
609+
zip_ref.extractall(extract_dir)
610+
611+
# search recursively for .app folder so it will pick very first .app folder
612+
for root, dirs, files in os.walk(extract_dir):
613+
for d in dirs:
614+
if d.endswith(".app") and os.path.isdir(os.path.join(root, d)):
615+
return os.path.join(root, d)
616+
617+
return None
618+
619+
return None
620+
621+
622+
def extract_bundle_id_from_app(app_path: str) -> Optional[str]:
623+
"""
624+
Reads CFBundleIdentifier from Info.plist inside .app bundle
625+
"""
626+
info_plist_path = os.path.join(app_path, "Info.plist")
627+
628+
if not os.path.exists(info_plist_path):
629+
return None
630+
631+
try:
632+
with open(info_plist_path, "rb") as f:
633+
plist_data = plistlib.load(f)
634+
return plist_data.get("CFBundleIdentifier")
635+
except Exception:
636+
return None
637+
638+
639+
@router.post("/ios/app-install")
640+
def handle_ios_app_install(filename: str, sim_udid: str):
641+
"""
642+
handling the ios app installation in the simolator
643+
"""
644+
dirpath = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app"
645+
filepath = os.path.join(dirpath, filename)
646+
if not os.path.exists(filepath):
647+
return {"message": "App not found", "filename": filename}
648+
649+
app_path = normalized_ios_app_path(filepath)
650+
if not app_path:
651+
return {"message": "Failed to normalize .app", "filename": filename}
652+
653+
bundle_id = extract_bundle_id_from_app(app_path)
654+
655+
try:
656+
# uninstall if already exists
657+
try:
658+
subprocess.run(
659+
["xcrun", "simctl", "uninstall", sim_udid, bundle_id],
660+
capture_output=True, text=True, timeout=30
661+
)
662+
except:
663+
pass # Ignore uninstall errors
664+
665+
# Install the app
666+
result = subprocess.run(
667+
["xcrun", "simctl", "install", sim_udid, app_path],
668+
capture_output=True, text=True, check=True, timeout=120
669+
)
670+
671+
# Verify installation
672+
verify_result = subprocess.run(
673+
["xcrun", "simctl", "get_app_container", sim_udid, bundle_id],
674+
capture_output=True, text=True, timeout=30
675+
)
676+
677+
if verify_result.returncode != 0:
678+
return {
679+
"message": f"App installed but verification failed: {verify_result.stderr}",
680+
"filename": filename,
681+
"bundle_id": bundle_id,
682+
}
683+
684+
return {
685+
"message": "App installed successfully",
686+
"filename": filename,
687+
"bundle_id": bundle_id,
688+
}
689+
except subprocess.CalledProcessError as e:
690+
return {
691+
"message": f"Error installing app: {e.stderr or str(e)}",
692+
"filename": filename,
693+
"bundle_id": bundle_id,
694+
}
695+
696+
697+
@router.get("/ios/bundle-installed")
698+
def is_ios_app_installed(sim_udid: str, bundle_id: str):
699+
try:
700+
result = subprocess.run(
701+
["xcrun", "simctl", "get_app_container", sim_udid, bundle_id],
702+
capture_output=True, text=True, timeout=10
703+
)
704+
705+
if result.returncode == 0 and result.stdout.strip():
706+
list_result = subprocess.run(
707+
["xcrun", "simctl", "listapps", sim_udid],
708+
capture_output=True, text=True, timeout=10
709+
)
710+
711+
if list_result.returncode == 0:
712+
return {"installed": bundle_id in list_result.stdout}
713+
714+
return {"installed": True}
715+
716+
return {"installed": False}
717+
except Exception as e:
718+
return {"installed": False, "error": str(e)}

0 commit comments

Comments
 (0)