mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-02-27 12:22:43 -08:00
323 lines
9.5 KiB
Python
Executable File
323 lines
9.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
iOS App Launcher - App Lifecycle Control
|
|
|
|
Launches, terminates, and manages iOS apps in the simulator.
|
|
Handles deep links and app switching.
|
|
|
|
Usage: python scripts/app_launcher.py --launch com.example.app
|
|
"""
|
|
|
|
import argparse
|
|
import contextlib
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
from common import build_simctl_command, resolve_udid
|
|
|
|
|
|
class AppLauncher:
|
|
"""Controls app lifecycle on iOS simulator."""
|
|
|
|
def __init__(self, udid: str | None = None):
|
|
"""Initialize app launcher."""
|
|
self.udid = udid
|
|
|
|
def launch(self, bundle_id: str, wait_for_debugger: bool = False) -> tuple[bool, int | None]:
|
|
"""
|
|
Launch an app.
|
|
|
|
Args:
|
|
bundle_id: App bundle identifier
|
|
wait_for_debugger: Wait for debugger attachment
|
|
|
|
Returns:
|
|
(success, pid) tuple
|
|
"""
|
|
cmd = build_simctl_command("launch", self.udid, bundle_id)
|
|
|
|
if wait_for_debugger:
|
|
cmd.insert(3, "--wait-for-debugger") # Insert after "launch" operation
|
|
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
# Parse PID from output if available
|
|
pid = None
|
|
if result.stdout:
|
|
# Output format: "com.example.app: <PID>"
|
|
parts = result.stdout.strip().split(":")
|
|
if len(parts) > 1:
|
|
with contextlib.suppress(ValueError):
|
|
pid = int(parts[1].strip())
|
|
return (True, pid)
|
|
except subprocess.CalledProcessError:
|
|
return (False, None)
|
|
|
|
def terminate(self, bundle_id: str) -> bool:
|
|
"""
|
|
Terminate an app.
|
|
|
|
Args:
|
|
bundle_id: App bundle identifier
|
|
|
|
Returns:
|
|
Success status
|
|
"""
|
|
cmd = build_simctl_command("terminate", self.udid, bundle_id)
|
|
|
|
try:
|
|
subprocess.run(cmd, capture_output=True, check=True)
|
|
return True
|
|
except subprocess.CalledProcessError:
|
|
return False
|
|
|
|
def install(self, app_path: str) -> bool:
|
|
"""
|
|
Install an app.
|
|
|
|
Args:
|
|
app_path: Path to .app bundle
|
|
|
|
Returns:
|
|
Success status
|
|
"""
|
|
cmd = build_simctl_command("install", self.udid, app_path)
|
|
|
|
try:
|
|
subprocess.run(cmd, capture_output=True, check=True)
|
|
return True
|
|
except subprocess.CalledProcessError:
|
|
return False
|
|
|
|
def uninstall(self, bundle_id: str) -> bool:
|
|
"""
|
|
Uninstall an app.
|
|
|
|
Args:
|
|
bundle_id: App bundle identifier
|
|
|
|
Returns:
|
|
Success status
|
|
"""
|
|
cmd = build_simctl_command("uninstall", self.udid, bundle_id)
|
|
|
|
try:
|
|
subprocess.run(cmd, capture_output=True, check=True)
|
|
return True
|
|
except subprocess.CalledProcessError:
|
|
return False
|
|
|
|
def open_url(self, url: str) -> bool:
|
|
"""
|
|
Open URL (for deep linking).
|
|
|
|
Args:
|
|
url: URL to open (http://, myapp://, etc.)
|
|
|
|
Returns:
|
|
Success status
|
|
"""
|
|
cmd = build_simctl_command("openurl", self.udid, url)
|
|
|
|
try:
|
|
subprocess.run(cmd, capture_output=True, check=True)
|
|
return True
|
|
except subprocess.CalledProcessError:
|
|
return False
|
|
|
|
def list_apps(self) -> list[dict[str, str]]:
|
|
"""
|
|
List installed apps.
|
|
|
|
Returns:
|
|
List of app info dictionaries
|
|
"""
|
|
cmd = build_simctl_command("listapps", self.udid)
|
|
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
|
|
# Parse plist output using plutil to convert to JSON
|
|
plist_data = result.stdout
|
|
|
|
# Use plutil to convert plist to JSON
|
|
convert_cmd = ["plutil", "-convert", "json", "-o", "-", "-"]
|
|
convert_result = subprocess.run(
|
|
convert_cmd, check=False, input=plist_data, capture_output=True, text=True
|
|
)
|
|
|
|
apps = []
|
|
if convert_result.returncode == 0:
|
|
import json
|
|
|
|
try:
|
|
data = json.loads(convert_result.stdout)
|
|
for bundle_id, app_info in data.items():
|
|
# Skip system internal apps that are hidden
|
|
if app_info.get("ApplicationType") == "Hidden":
|
|
continue
|
|
|
|
apps.append(
|
|
{
|
|
"bundle_id": bundle_id,
|
|
"name": app_info.get(
|
|
"CFBundleDisplayName", app_info.get("CFBundleName", bundle_id)
|
|
),
|
|
"path": app_info.get("Path", ""),
|
|
"version": app_info.get("CFBundleVersion", "Unknown"),
|
|
"type": app_info.get("ApplicationType", "User"),
|
|
}
|
|
)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
return apps
|
|
except subprocess.CalledProcessError:
|
|
return []
|
|
|
|
def get_app_state(self, bundle_id: str) -> str:
|
|
"""
|
|
Get app state (running, suspended, etc.).
|
|
|
|
Args:
|
|
bundle_id: App bundle identifier
|
|
|
|
Returns:
|
|
State string or 'unknown'
|
|
"""
|
|
# Check if app is running by trying to get its PID
|
|
cmd = build_simctl_command("spawn", self.udid, "launchctl", "list")
|
|
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
if bundle_id in result.stdout:
|
|
return "running"
|
|
return "not running"
|
|
except subprocess.CalledProcessError:
|
|
return "unknown"
|
|
|
|
def restart_app(self, bundle_id: str, delay: float = 1.0) -> bool:
|
|
"""
|
|
Restart an app (terminate then launch).
|
|
|
|
Args:
|
|
bundle_id: App bundle identifier
|
|
delay: Delay between terminate and launch
|
|
|
|
Returns:
|
|
Success status
|
|
"""
|
|
# Terminate
|
|
self.terminate(bundle_id)
|
|
time.sleep(delay)
|
|
|
|
# Launch
|
|
success, _ = self.launch(bundle_id)
|
|
return success
|
|
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
parser = argparse.ArgumentParser(description="Control iOS app lifecycle")
|
|
|
|
# Actions
|
|
parser.add_argument("--launch", help="Launch app by bundle ID")
|
|
parser.add_argument("--terminate", help="Terminate app by bundle ID")
|
|
parser.add_argument("--restart", help="Restart app by bundle ID")
|
|
parser.add_argument("--install", help="Install app from .app path")
|
|
parser.add_argument("--uninstall", help="Uninstall app by bundle ID")
|
|
parser.add_argument("--open-url", help="Open URL (deep link)")
|
|
parser.add_argument("--list", action="store_true", help="List installed apps")
|
|
parser.add_argument("--state", help="Get app state by bundle ID")
|
|
|
|
# Options
|
|
parser.add_argument(
|
|
"--wait-for-debugger", action="store_true", help="Wait for debugger when launching"
|
|
)
|
|
parser.add_argument(
|
|
"--udid",
|
|
help="Device UDID (auto-detects booted simulator if not provided)",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Resolve UDID with auto-detection
|
|
try:
|
|
udid = resolve_udid(args.udid)
|
|
except RuntimeError as e:
|
|
print(f"Error: {e}")
|
|
sys.exit(1)
|
|
|
|
launcher = AppLauncher(udid=udid)
|
|
|
|
# Execute requested action
|
|
if args.launch:
|
|
success, pid = launcher.launch(args.launch, args.wait_for_debugger)
|
|
if success:
|
|
if pid:
|
|
print(f"Launched {args.launch} (PID: {pid})")
|
|
else:
|
|
print(f"Launched {args.launch}")
|
|
else:
|
|
print(f"Failed to launch {args.launch}")
|
|
sys.exit(1)
|
|
|
|
elif args.terminate:
|
|
if launcher.terminate(args.terminate):
|
|
print(f"Terminated {args.terminate}")
|
|
else:
|
|
print(f"Failed to terminate {args.terminate}")
|
|
sys.exit(1)
|
|
|
|
elif args.restart:
|
|
if launcher.restart_app(args.restart):
|
|
print(f"Restarted {args.restart}")
|
|
else:
|
|
print(f"Failed to restart {args.restart}")
|
|
sys.exit(1)
|
|
|
|
elif args.install:
|
|
if launcher.install(args.install):
|
|
print(f"Installed {args.install}")
|
|
else:
|
|
print(f"Failed to install {args.install}")
|
|
sys.exit(1)
|
|
|
|
elif args.uninstall:
|
|
if launcher.uninstall(args.uninstall):
|
|
print(f"Uninstalled {args.uninstall}")
|
|
else:
|
|
print(f"Failed to uninstall {args.uninstall}")
|
|
sys.exit(1)
|
|
|
|
elif args.open_url:
|
|
if launcher.open_url(args.open_url):
|
|
print(f"Opened URL: {args.open_url}")
|
|
else:
|
|
print(f"Failed to open URL: {args.open_url}")
|
|
sys.exit(1)
|
|
|
|
elif args.list:
|
|
apps = launcher.list_apps()
|
|
if apps:
|
|
print(f"Installed apps ({len(apps)}):")
|
|
for app in apps[:10]: # Limit for token efficiency
|
|
print(f" {app['bundle_id']}: {app['name']} (v{app['version']})")
|
|
if len(apps) > 10:
|
|
print(f" ... and {len(apps) - 10} more")
|
|
else:
|
|
print("No apps found or failed to list")
|
|
|
|
elif args.state:
|
|
state = launcher.get_app_state(args.state)
|
|
print(f"{args.state}: {state}")
|
|
|
|
else:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|